diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index 8a4c4bd..48967a2 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index b868058..cfea19c 100644 Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and b/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/app/src/main/java/com/vectras/boxvidra/activities/SplashActivity.java b/app/src/main/java/com/vectras/boxvidra/activities/SplashActivity.java index 06191e3..0bee4be 100644 --- a/app/src/main/java/com/vectras/boxvidra/activities/SplashActivity.java +++ b/app/src/main/java/com/vectras/boxvidra/activities/SplashActivity.java @@ -205,7 +205,7 @@ private boolean checkFreeSpace() { } private void showLowSpaceDialog() { - new AlertDialog.Builder(activity) + new AlertDialog.Builder(activity, R.style.MainDialogTheme) .setTitle("Low Space Warning") .setMessage("You do not have enough free space to continue.") .setPositiveButton("Exit", (dialog, which) -> finish()) diff --git a/app/src/main/java/com/vectras/boxvidra/adapters/WinePrefixAdapter.java b/app/src/main/java/com/vectras/boxvidra/adapters/WinePrefixAdapter.java index 13b956c..55ac170 100644 --- a/app/src/main/java/com/vectras/boxvidra/adapters/WinePrefixAdapter.java +++ b/app/src/main/java/com/vectras/boxvidra/adapters/WinePrefixAdapter.java @@ -1,11 +1,11 @@ package com.vectras.boxvidra.adapters; +import android.app.ActivityManager; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.system.ErrnoException; import android.text.SpannableString; import android.text.Spanned; @@ -14,6 +14,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; @@ -21,14 +22,19 @@ import androidx.appcompat.app.AlertDialog; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.switchmaterial.SwitchMaterial; import com.vectras.boxvidra.R; import com.vectras.boxvidra.activities.MainActivity; import com.vectras.boxvidra.core.TermuxX11; -import com.vectras.boxvidra.fragments.HomeFragment; import com.vectras.boxvidra.services.MainService; import com.vectras.boxvidra.utils.BoxvidraUtils; +import com.vectras.boxvidra.utils.JsonUtils; + +import org.json.JSONException; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.util.List; public class WinePrefixAdapter extends RecyclerView.Adapter { @@ -48,10 +54,76 @@ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { return new ViewHolder(view); } + private boolean isMainServiceRunning() { + ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { + if (MainService.class.getName().equals(service.service.getClassName())) { + return true; + } + } + return false; + } + + private void showOptionsDialog(ViewHolder holder, File winePrefix) { + View dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_prefix_options, null); + + SwitchMaterial switchWine64 = dialogView.findViewById(R.id.switchWine64); + SwitchMaterial switchStartXFCE4 = dialogView.findViewById(R.id.switchStartXFCE4); + + // Load saved options if available + File optionsFile = new File(winePrefix, "options.json"); + if (optionsFile.exists()) { + try { + JSONObject options = JsonUtils.loadOptionsFromJson(optionsFile); + switchWine64.setChecked(options.getBoolean("wine64")); + switchStartXFCE4.setChecked(options.getBoolean("startxfce4")); + } catch (IOException | JSONException e) { + e.printStackTrace(); + } + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.MainDialogTheme) + .setTitle("Choose Command") + .setView(dialogView) + .setPositiveButton("OK", (dialog, which) -> { + if (!switchWine64.isChecked() && !switchStartXFCE4.isChecked()) { + Toast.makeText(context, "At least one option must be enabled!", Toast.LENGTH_SHORT).show(); + showOptionsDialog(holder, winePrefix); + } else { + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("wine64", switchWine64.isChecked()); + jsonObject.put("startxfce4", switchStartXFCE4.isChecked()); + + JsonUtils.saveOptionsToJson(optionsFile, jsonObject); + } catch (JSONException | IOException e) { + e.printStackTrace(); + Toast.makeText(context, "Failed to save options!", Toast.LENGTH_SHORT).show(); + } + } + }) + .setNegativeButton("Cancel", null); + + AlertDialog dialog = builder.create(); + dialog.show(); + } + @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { File winePrefix = winePrefixes.get(position); holder.textView.setText(winePrefix.getName()); + + boolean isServiceRunning = isMainServiceRunning(); + holder.itemView.setEnabled(!isServiceRunning); + holder.menuButton.setEnabled(!isServiceRunning); + + if (isServiceRunning) { + holder.itemView.setOnClickListener(null); + holder.itemView.setOnLongClickListener(null); + } else { + holder.itemView.setOnClickListener(holder); + holder.itemView.setOnLongClickListener(holder); + } } @Override @@ -61,10 +133,19 @@ public int getItemCount() { public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { TextView textView; + ImageButton menuButton; public ViewHolder(@NonNull View itemView) { super(itemView); textView = itemView.findViewById(R.id.TVTitle); + menuButton = itemView.findViewById(R.id.BTMenu); + + menuButton.setOnClickListener(v -> { + int position = getAdapterPosition(); + File winePrefix = winePrefixes.get(position); + showOptionsDialog(this, winePrefix); + }); + itemView.setOnClickListener(this); itemView.setOnLongClickListener(this); } @@ -159,4 +240,4 @@ private void deleteDirectory(File dir) { } dir.delete(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vectras/boxvidra/fragments/OptionsFragment.java b/app/src/main/java/com/vectras/boxvidra/fragments/OptionsFragment.java index 09964f1..222d5d0 100644 --- a/app/src/main/java/com/vectras/boxvidra/fragments/OptionsFragment.java +++ b/app/src/main/java/com/vectras/boxvidra/fragments/OptionsFragment.java @@ -1,8 +1,10 @@ package com.vectras.boxvidra.fragments; +import android.content.Intent; import android.os.Bundle; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import com.termux.app.TermuxActivity; import com.vectras.boxvidra.R; public class OptionsFragment extends PreferenceFragmentCompat { @@ -11,15 +13,17 @@ public class OptionsFragment extends PreferenceFragmentCompat { public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preferences, rootKey); - // Handle clicks on preferences - findPreference("key_environment").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - // Navigate to EnvironmentManagementFragment - navigateToEnvironmentManagementFragment(); - return true; - } + findPreference("key_environment").setOnPreferenceClickListener(preference -> { + navigateToEnvironmentManagementFragment(); + return true; }); + + findPreference("key_terminal").setOnPreferenceClickListener(preference -> { + Intent intent = new Intent(getActivity(), TermuxActivity.class); + startActivity(intent); + return true; + }); + } private void navigateToEnvironmentManagementFragment() { diff --git a/app/src/main/java/com/vectras/boxvidra/fragments/WinePrefixFragment.java b/app/src/main/java/com/vectras/boxvidra/fragments/WinePrefixFragment.java index 63958b0..355c3dd 100644 --- a/app/src/main/java/com/vectras/boxvidra/fragments/WinePrefixFragment.java +++ b/app/src/main/java/com/vectras/boxvidra/fragments/WinePrefixFragment.java @@ -28,9 +28,14 @@ import com.termux.app.TermuxService; import com.vectras.boxvidra.R; import com.vectras.boxvidra.adapters.WinePrefixAdapter; +import com.vectras.boxvidra.utils.JsonUtils; + +import org.json.JSONException; +import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -164,6 +169,18 @@ protected Boolean doInBackground(String... params) { new File(prefixDir, "dosdevices").mkdirs(); new File(prefixDir, "dosdevices/c:").mkdirs(); new File(prefixDir, "dosdevices/d:").mkdirs(); + + // Create options.json with both options enabled + File optionsFile = new File(prefixDir, "options.json"); + JSONObject jsonObject = new JSONObject(); + try { + jsonObject.put("wine64", false); + jsonObject.put("startxfce4", true); + JsonUtils.saveOptionsToJson(optionsFile, jsonObject); + } catch (JSONException | IOException e) { + e.printStackTrace(); + return false; + } return true; } else { return false; @@ -182,4 +199,12 @@ protected void onPostExecute(Boolean success) { } } } + + @Override + public void onResume() { + super.onResume(); + loadWinePrefixes(); + adapter.notifyDataSetChanged(); + } + } diff --git a/app/src/main/java/com/vectras/boxvidra/services/MainService.java b/app/src/main/java/com/vectras/boxvidra/services/MainService.java index 96f2c89..81053a3 100644 --- a/app/src/main/java/com/vectras/boxvidra/services/MainService.java +++ b/app/src/main/java/com/vectras/boxvidra/services/MainService.java @@ -3,21 +3,19 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; +import android.app.PendingIntent; import android.app.Service; import android.content.ActivityNotFoundException; import android.content.Intent; import android.os.Build; import android.os.IBinder; -import android.system.ErrnoException; import android.util.Log; import androidx.core.app.NotificationCompat; import com.termux.app.TermuxService; import com.vectras.boxvidra.R; -import com.vectras.boxvidra.activities.MainActivity; import com.vectras.boxvidra.core.SoundThread; -import com.vectras.boxvidra.core.TermuxX11; import com.vectras.boxvidra.utils.BoxvidraUtils; import java.io.BufferedReader; @@ -30,8 +28,6 @@ import java.net.SocketException; import java.util.Enumeration; -import android.app.PendingIntent; - public class MainService extends Service { private static final String TAG = "MainService"; private static final String CHANNEL_ID = "MainServiceChannel"; @@ -40,27 +36,33 @@ public class MainService extends Service { private SoundThread soundThread; private Thread thread; + private Process prootProcess; + @Override public int onStartCommand(Intent intent, int flags, int startId) { createNotificationChannel(); - Intent notificationIntent = new Intent(this, MainActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); + Intent killAllIntent = new Intent(this, MainService.class); + killAllIntent.setAction("KILL_ALL"); + PendingIntent killAllPendingIntent = PendingIntent.getService(this, 0, killAllIntent, PendingIntent.FLAG_UPDATE_CURRENT); - Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID) + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("Boxvidra") - .setContentText("Boxvidra running in background") + .setContentText("Pull down to show options") .setSmallIcon(R.drawable.ic_main_service_icon) - .setContentIntent(pendingIntent) - .build(); + .addAction(R.drawable.round_logout_24, "Stop All Processes", killAllPendingIntent); + + Notification notification = notificationBuilder.build(); startForeground(NOTIFICATION_ID, notification); String commandLine = BoxvidraUtils.BoxvidraCmdLine(getApplicationContext()); - if (commandLine != null) + if (commandLine != null) { + executeProotCommand("virgl_test_server_android"); executeProotCommand(commandLine); - else + } else { stopSelf(); + } Intent x11Intent = new Intent(); x11Intent.setClassName("com.termux.x11", "com.termux.x11.MainActivity"); @@ -72,32 +74,28 @@ public int onStartCommand(Intent intent, int flags, int startId) { Log.e("LaunchActivity", "Activity not found: " + e.getMessage()); } - //startAudio(ip, port); + if (intent != null && "KILL_ALL".equals(intent.getAction())) { + stopAllProcesses(); + } return START_NOT_STICKY; } - private String getLocalIpAddress() { - try { - for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { - NetworkInterface intf = en.nextElement(); - for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { - InetAddress inetAddress = enumIpAddr.nextElement(); - if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().contains(".")) { - return inetAddress.getHostAddress().toString(); - } - } - } - } catch (SocketException ex) { - ex.printStackTrace(); + private void createNotificationChannel() { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, + "MainService Channel", + NotificationManager.IMPORTANCE_DEFAULT + ); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(channel); } - return null; } private void startAudio(String ip, String port) { if (soundThread == null) { soundThread = new SoundThread(ip, port, error -> { - // Handle errors here Log.e(TAG, "Error: " + error); }); thread = new Thread(soundThread); @@ -158,35 +156,30 @@ private void executeProotCommand(String command) { processBuilder.command(prootCommand); - Log.d(TAG, "Proot command: " + String.join(" ", prootCommand)); // Log proot command + prootProcess = processBuilder.start(); + Log.d(TAG, "Proot process started"); - Process process = processBuilder.start(); - BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream())); - BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); - BufferedReader errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(prootProcess.getOutputStream())); + BufferedReader reader = new BufferedReader(new InputStreamReader(prootProcess.getInputStream())); + BufferedReader errorReader = new BufferedReader(new InputStreamReader(prootProcess.getErrorStream())); writer.write(command); writer.newLine(); writer.flush(); - writer.close(); - - Log.d(TAG, "Proot write in command: " + command); // Log proot write in command String line; while ((line = reader.readLine()) != null) { - Log.d(TAG, line); // Log output from proot + Log.d(TAG, line); } - // Read any errors from the error stream while ((line = errorReader.readLine()) != null) { - Log.e(TAG, line); // Log errors from proot + Log.e(TAG, line); } - // Wait for the process to finish - int exitValue = process.waitFor(); + int exitValue = prootProcess.waitFor(); Log.d(TAG, "Command execution finished with exit code: " + exitValue); - // Clean up + writer.close(); reader.close(); errorReader.close(); @@ -196,17 +189,27 @@ private void executeProotCommand(String command) { }).start(); } - private void createNotificationChannel() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationChannel channel = new NotificationChannel( - CHANNEL_ID, - "MainService Channel", - NotificationManager.IMPORTANCE_DEFAULT - ); - NotificationManager manager = getSystemService(NotificationManager.class); - if (manager != null) { - manager.createNotificationChannel(channel); + private void stopAllProcesses() { + stopAudio(); + stopSelf(); + System.exit(0); + } + + private String getLocalIpAddress() { + try { + for (Enumeration en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) { + NetworkInterface intf = en.nextElement(); + for (Enumeration enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (!inetAddress.isLoopbackAddress() && inetAddress.getHostAddress().toString().contains(".")) { + return inetAddress.getHostAddress().toString(); + } + } } + } catch (SocketException ex) { + ex.printStackTrace(); } + return null; } } + diff --git a/app/src/main/java/com/vectras/boxvidra/utils/BoxvidraUtils.java b/app/src/main/java/com/vectras/boxvidra/utils/BoxvidraUtils.java index 84fadc1..ac2f88e 100644 --- a/app/src/main/java/com/vectras/boxvidra/utils/BoxvidraUtils.java +++ b/app/src/main/java/com/vectras/boxvidra/utils/BoxvidraUtils.java @@ -6,8 +6,12 @@ import com.termux.app.TermuxService; import com.vectras.boxvidra.fragments.EnvironmentVariablesFragment; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.IOException; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; @@ -18,12 +22,10 @@ public static String BoxvidraCmdLine(Context context) { if (prefixName == null) return null; - // Retrieve saved environment variables from SharedPreferences SharedPreferences sharedPreferences = context.getSharedPreferences(EnvironmentVariablesFragment.PREFS_NAME, Context.MODE_PRIVATE); Set savedVars = sharedPreferences.getStringSet(EnvironmentVariablesFragment.ENVIRONMENT_VARS_KEY, new HashSet<>()); ArrayList environmentVariables = new ArrayList<>(savedVars); - // Build the command with environment variables ArrayList command = new ArrayList<>(); for (String var : environmentVariables) { @@ -36,10 +38,23 @@ public static String BoxvidraCmdLine(Context context) { command.add("export WINEPREFIX='" + TermuxService.OPT_PATH + "/wine-prefixes/" + prefixName + "'"); command.add(";"); - command.add("xfce4-session"); + // Retrieve command options from JSON + File prefixDir = new File(TermuxService.OPT_PATH + "/wine-prefixes/" + prefixName); + File optionsFile = new File(prefixDir, "options.json"); + try { + if (optionsFile.exists()) { + JSONObject options = JsonUtils.loadOptionsFromJson(optionsFile); + if (options.getBoolean("wine64")) { + command.add("wine64"); + } + if (options.getBoolean("startxfce4")) { + command.add("startxfce4"); + } + } + } catch (IOException | JSONException e) { + e.printStackTrace(); + } return String.join(" ", command); } } - - diff --git a/app/src/main/java/com/vectras/boxvidra/utils/JsonUtils.java b/app/src/main/java/com/vectras/boxvidra/utils/JsonUtils.java new file mode 100644 index 0000000..a9dedef --- /dev/null +++ b/app/src/main/java/com/vectras/boxvidra/utils/JsonUtils.java @@ -0,0 +1,33 @@ +package com.vectras.boxvidra.utils; + +import org.json.JSONException; +import org.json.JSONObject; +import java.io.File; +import java.io.FileWriter; +import java.io.FileReader; +import java.io.IOException; +import java.io.BufferedReader; + +public class JsonUtils { + + public static void saveOptionsToJson(File file, JSONObject jsonObject) throws IOException { + try (FileWriter fileWriter = new FileWriter(file)) { + fileWriter.write(jsonObject.toString()); + } + } + + public static JSONObject loadOptionsFromJson(File file) throws IOException { + StringBuilder jsonContent = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(file))) { + String line; + while ((line = reader.readLine()) != null) { + jsonContent.append(line); + } + } + try { + return new JSONObject(jsonContent.toString()); + } catch (JSONException e) { + throw new RuntimeException(e); + } + } +} diff --git a/app/src/main/res/drawable/round_build_24.xml b/app/src/main/res/drawable/round_build_24.xml new file mode 100644 index 0000000..ad63c65 --- /dev/null +++ b/app/src/main/res/drawable/round_build_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/dialog_prefix_options.xml b/app/src/main/res/layout/dialog_prefix_options.xml new file mode 100644 index 0000000..fdb31f7 --- /dev/null +++ b/app/src/main/res/layout/dialog_prefix_options.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/app/src/main/res/layout/item_wine_prefix.xml b/app/src/main/res/layout/item_wine_prefix.xml index 5df3280..a082629 100644 --- a/app/src/main/res/layout/item_wine_prefix.xml +++ b/app/src/main/res/layout/item_wine_prefix.xml @@ -52,7 +52,6 @@ android:gravity="left" android:padding="10dp" android:src="@drawable/round_settings_24" - android:visibility="gone" app:tint="?attr/colorPrimary" /> @@ -70,4 +69,4 @@ android:textSize="20sp" android:textStyle="bold" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/material_preference.xml b/app/src/main/res/layout/material_preference.xml index 46e4fbf..5f52b32 100644 --- a/app/src/main/res/layout/material_preference.xml +++ b/app/src/main/res/layout/material_preference.xml @@ -5,7 +5,6 @@ android:layout_height="wrap_content" android:focusable="true" android:padding="18dp" - android:layout_margin="8dp" android:foreground="?android:attr/selectableItemBackground" android:clickable="true"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b937b19..f35fc53 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -83,4 +83,8 @@ Add new prefixes Prefix Name Machine Thumbnail + Wine64 + Start XFCE4 + Tools + Terminal \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 8b08d55..9bf0d84 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -14,4 +14,16 @@ + + + + + +