- Added quick switching between running virtual machines to the VNC screen.
- Removed the option button from the virtual machine list, replaced by a long press.
- Fixed issues where other virtual machines would unexpectedly shut down when the first one was shut down.
- Improved the interface.
- Bugs fixed.
This commit is contained in:
An Bui 2026-05-07 11:12:17 +07:00
parent aeff4a5a4b
commit 6c73672230
21 changed files with 262 additions and 32 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId "com.vectras.vm"
minSdk minApi
targetSdk targetApi
versionCode 118
versionName "4.1.4"
versionCode 119
versionName "4.1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true

View file

@ -157,6 +157,10 @@ public class Config {
return Config.getCacheDir()+ "/" + vmID + "/vncsocket";
}
public static String getLocalVNCSocketPath(String vmId) {
return Config.getCacheDir()+ "/" + vmId + "/vncsocket";
}
public static String getCacheVMPath() {
return Config.getCacheDir()+ "/" + vmID + "/";
}

View file

@ -864,7 +864,6 @@ public class MainVNCActivity extends VncCanvasActivity {
bindingSendKey.sendtextEdittext.setEnabled(false);
QmpSender.quickShutdown();
Config.setDefault();
MainService.stopService();
finish();
}

View file

@ -29,6 +29,7 @@ import com.vectras.vm.main.vms.DataMainRoms;
import com.vectras.vm.manager.QmpSender;
import com.vectras.vm.manager.VmFileManager;
import com.vectras.vm.manager.VmActions;
import com.vectras.vm.manager.VmServiceManager;
import com.vectras.vm.settings.X11DisplaySettingsActivity;
import com.vectras.vm.utils.DialogUtils;
import com.vectras.vm.utils.FileUtils;
@ -657,6 +658,8 @@ public class VMManager {
return false;
}
VmServiceManager.stopService(_activity);
if (_command.contains("qemu-system") && _result.contains("Killed")) {
isQemuStopedWithError = true;
return true;

View file

@ -16,7 +16,7 @@ import com.vectras.vm.manager.VmAudioManager;
import com.vectras.vm.manager.VmControllerDialog;
public class RomOptionsDialog {
public static void showNow(Activity activity, int position, String vmID, String vmName) {
public static void show(Activity activity, int position, String vmID, String vmName) {
if (VMManager.isVMRunning(activity, vmID)) {
VmControllerDialog vmControllerDialog = new VmControllerDialog();
vmControllerDialog.streamAudio = VmAudioManager.streamAudio;

View file

@ -55,7 +55,7 @@ public class SoftwareStoreHomeAdapter extends RecyclerView.Adapter<RecyclerView.
// Get current position of item in recyclerview to bind data and assign values from list
final MyHolder myHolder = (MyHolder) holder;
final DataRoms current = displayList.get(position);
Glide.with(context).load(current.romIcon).override(180, 180).placeholder(R.drawable.ic_computer_180dp_with_padding).error(R.drawable.ic_computer_180dp_with_padding).into(myHolder.ivIcon);
Glide.with(context).load(current.romIcon).override(180, 180).placeholder(R.drawable.ic_deployed_code_180px_with_padding).error(R.drawable.ic_deployed_code_180px_with_padding).into(myHolder.ivIcon);
myHolder.textName.setText(current.romName);
myHolder.textSize.setText(current.romSize);
if (current.romAvail) {

View file

@ -16,7 +16,6 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.vectras.qemu.Config;
import com.vectras.vm.AppConfig;
import com.vectras.vm.R;
import com.vectras.vm.StartVM;
import com.vectras.vm.VMManager;
@ -78,7 +77,6 @@ public class VmsHomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
} else {
VMManager.setIconWithName(myHolder.ivIcon, current.itemName);
}
myHolder.optionsBtn.setOnClickListener(view -> RomOptionsDialog.showNow(activity, position, current.vmID, current.itemName));
myHolder.cdRoms.setOnClickListener(view -> {
VMManager.setArch(current.itemArch, activity);
@ -96,8 +94,8 @@ public class VmsHomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
});
myHolder.cdRoms.setOnLongClickListener(v -> {
VMManager.deleteVMDialog(current.itemName, position, activity);
return false;
RomOptionsDialog.show(activity, position, current.vmID, current.itemName);
return true;
});
}

View file

@ -2,6 +2,7 @@ package com.vectras.vm.manager;
import android.androidVNC.ConnectionBean;
import android.androidVNC.VncCanvas;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
@ -36,6 +37,8 @@ import com.vectras.vm.utils.ProgressDialog;
import com.vectras.vm.utils.StreamAudio;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -109,6 +112,41 @@ public class VmControllerDialog extends DialogFragment {
}
dismiss();
});
binding.lnSwitch.setVisibility(View.GONE);
} else if (requireActivity() instanceof MainVNCActivity) {
ArrayList<HashMap<String, Object>> list = VmListManager.getAllVmForPickRunningVncSocketOnly(requireActivity());
if (list.isEmpty() || (list.size() == 1 && Objects.requireNonNull(list.get(0).get("value")).toString().equals(Config.vmID))) {
binding.lnManage.setVisibility(View.GONE);
} else {
binding.lnSwitch.setOnClickListener(v -> {
if (isAdded()) {
VmPicker vmPicker = new VmPicker(requireActivity());
vmPicker.currentVmId= Config.vmID;
vmPicker.listVm = list;
vmPicker.pick((position, name, value) -> {
if (Config.vmID.equals(value)) return;
if (position < 0) {
DialogUtils.oopsDialog(requireActivity(), getString(R.string.no_vms_are_available));
return;
}
Config.vmID = value;
streamAudio.setCross(null);
streamAudio.stop();
requireActivity().recreate();
dismiss();
});
}
});
binding.lnConnect.setVisibility(View.GONE);
binding.lnEdit.setVisibility(View.GONE);
binding.lnRemove.setVisibility(View.GONE);
}
} else {
binding.lnManage.setVisibility(View.GONE);
}

View file

@ -0,0 +1,67 @@
package com.vectras.vm.manager;
import android.content.Context;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.vectras.qemu.Config;
import com.vectras.vm.AppConfig;
import com.vectras.vm.creator.ListManager;
import com.vectras.vm.main.vms.DataMainRoms;
import com.vectras.vm.utils.FileUtils;
import java.io.File;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
public class VmListManager {
public static ArrayList<HashMap<String, Object>> getAllVmForPickRunningVncSocketOnly(Context context) {
ArrayList<HashMap<String, Object>> listVm = getAllVmForPick(context);
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
for (int i = 0; i < listVm.size(); i++) {
if (FileUtils.isFileExists(Config.getLocalVNCSocketPath(Objects.requireNonNull(listVm.get(i).get("value")).toString()))) {
list.add(listVm.get(i));
}
}
return list;
}
public static ArrayList<HashMap<String, Object>> getAllVmForPickRunningOnly(Context context) {
ArrayList<HashMap<String, Object>> listVm = getAllVmForPick(context);
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
for (int i = 0; i < listVm.size(); i++) {
if (FileUtils.isFileExists(Config.getLocalQMPSocketPath(Objects.requireNonNull(listVm.get(i).get("value")).toString()))) {
list.add(listVm.get(i));
}
}
return list;
}
public static ArrayList<HashMap<String, Object>> getAllVmForPick(Context context) {
ArrayList<HashMap<String, Object>> list = new ArrayList<>();
List<DataMainRoms> data = getAllVm(context);
for (int i = 0; i < data.size(); i++) {
ListManager.putToList(list, data.get(i).itemName, data.get(i).vmID);
}
return list;
}
public static List<DataMainRoms> getAllVm(Context context) {
Gson gson = new Gson();
Type listType = new TypeToken<List<DataMainRoms>>(){}.getType();
String json = FileUtils.readFromFile(context, new File(AppConfig.romsdatajson));
return gson.fromJson(json, listType);
}
}

View file

@ -0,0 +1,49 @@
package com.vectras.vm.manager;
import android.app.Activity;
import com.vectras.vm.R;
import com.vectras.vm.creator.VMCreatorSelector;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
public class VmPicker {
public boolean isRunningOnly;
public String currentVmId = "";
private Activity activity;
ArrayList<HashMap<String, Object>> listVm;
public interface VMPickerCallback {
void onSelected(int position, String name, String vmId);
}
public VmPicker(Activity activity) {
this.activity = activity;
}
public void pick(VMPickerCallback callback) {
ArrayList<HashMap<String, Object>> list = listVm != null ? listVm : (isRunningOnly ? VmListManager.getAllVmForPickRunningOnly(activity) : VmListManager.getAllVmForPick(activity));
if (list.isEmpty()) callback.onSelected(-1, "", "");
int position = -1;
if (!currentVmId.isEmpty()) {
for (int i = 0; i < list.size(); i++) {
if (Objects.equals(list.get(i).get("value"), currentVmId)) {
position = i;
break;
}
}
}
VMCreatorSelector.showDialog(
activity,
list,
position,
(callback::onSelected),
activity.getString(R.string.switch_to));
}
}

View file

@ -0,0 +1,18 @@
package com.vectras.vm.manager;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import com.vectras.vm.MainService;
import com.vectras.vterm.Terminal;
public class VmServiceManager {
public static void stopService(Context context) {
new Thread(() -> {
if (!Terminal.executeShellCommandWithResult("ps -e", context).contains("qemu-system-")) {
new Handler(Looper.getMainLooper()).post(MainService::stopService);
}
}).start();
}
}

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M320,800L264,743L367,640L80,640L80,560L367,560L264,457L320,400L520,600L320,800ZM640,560L440,360L640,160L696,217L593,320L880,320L880,400L593,400L696,503L640,560Z"/>
</vector>

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="180dp"
android:height="180dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M440,777L440,503L200,364L200,638Q200,638 200,638Q200,638 200,638L440,777ZM520,777L760,638Q760,638 760,638Q760,638 760,638L760,364L520,503L520,777ZM480,434L717,297L480,160Q480,160 480,160Q480,160 480,160L243,297L480,434ZM160,708Q141,697 130.5,679Q120,661 120,639L120,321Q120,299 130.5,281Q141,263 160,252L440,91Q459,80 480,80Q501,80 520,91L800,252Q819,263 829.5,281Q840,299 840,321L840,639Q840,661 829.5,679Q819,697 800,708L520,869Q501,880 480,880Q459,880 440,869L160,708ZM480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,3 @@
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/deployed_code_180px"
android:inset="48dp" />

View file

@ -73,7 +73,9 @@
android:clickable="true"
android:focusable="true"
android:padding="8dp"
android:src="@drawable/more_vert_24px" />
android:src="@drawable/more_vert_24px"
android:visibility="gone"
tools:visibility="gone" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -90,6 +90,26 @@
android:textColor="?attr/colorError"
android:text="@string/remove"/>
</LinearLayout>
<LinearLayout
android:id="@+id/ln_switch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingVertical="8dp"
android:paddingHorizontal="16dp"
android:background="?attr/selectableItemBackground">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/compare_arrows_24px"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:textColor="?attr/colorControlNormal"
android:text="@string/switchtext"/>
</LinearLayout>
</LinearLayout>
<LinearLayout

View file

@ -569,6 +569,10 @@
<string name="edge_to_edge">Tràn cạnh</string>
<string name="disable_edge_to_edge">Tắt tràn cạnh</string>
<string name="pinch_to_zoom">Chụm ngón tay để thu phóng</string>
<string name="vm_picker">Trình chọn máy ảo</string>
<string name="switchtext">Chuyển đổi</string>
<string name="switch_to">Chuyển sang</string>
<string name="no_vms_are_available">Không có máy ảo nào khả dụng.</string>
<!--======================TERMUX STRINGS====================-->
<string name="application_name" translatable="false">Vterm</string>

View file

@ -580,6 +580,10 @@
<string name="edge_to_edge">Edge-to-edge</string>
<string name="disable_edge_to_edge">Disable edge-to-edge</string>
<string name="pinch_to_zoom">Pinch to zoom</string>
<string name="vm_picker">VM picker</string>
<string name="switchtext">Switch</string>
<string name="switch_to">Switch to</string>
<string name="no_vms_are_available">No virtual machines are available.</string>
<!--======================TERMUX STRINGS====================-->