- Listen to audio from the virtual machines on Vectras VM without Termux.
- Added a separate Use local time option for each virtual machine.
- Removed Boot from, Use UEFI and Use local time from the general Qemu settings.
- Automatically set a screenshot as the virtual machine thumbnail if the virtual machine doesn't have a thumbnail set.
- Fixed a bug where update checks weren't working.
- Added automatic cleanup of temporary files during setup.
- Added a separate folder for Quick Run and automatically cleaned it.
This commit is contained in:
An Bui 2026-04-20 17:46:22 +07:00
parent bafaa4d57a
commit febc93453c
23 changed files with 408 additions and 77 deletions

View file

@ -12,8 +12,8 @@ android {
applicationId "com.vectras.vm"
minSdk minApi
targetSdk targetApi
versionCode 101
versionName "3.9.7"
versionCode 102
versionName "3.9.8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true

View file

@ -153,6 +153,11 @@ public class Config {
return Config.getCacheDir() + "/" + vmID + "/qmpsocket";
}
public static String getLocalQMPSocketPath(String vmID) {
return Config.getCacheDir() + "/" + vmID + "/qmpsocket";
}
public static String getLocalVNCSocketPath() {
return Config.getCacheDir()+ "/" + vmID + "/vncsocket";
}

View file

@ -64,6 +64,7 @@ import com.vectras.vm.utils.FileUtils;
import com.vectras.vm.utils.ListUtils;
import com.vectras.vm.utils.NetworkUtils;
import com.vectras.vm.utils.SimulateKeyEvent;
import com.vectras.vm.utils.StreamAudio;
import com.vectras.vm.utils.UIUtils;
import java.io.File;
@ -105,6 +106,8 @@ public class MainVNCActivity extends VncCanvasActivity {
private final ArrayList<HashMap<String, Object>> listmapForSendKey = new ArrayList<>();
private boolean isConnected = false;
private StreamAudio streamAudio;
@Override
public void onCreate(Bundle b) {
super.onCreate(b);
@ -168,6 +171,9 @@ public class MainVNCActivity extends VncCanvasActivity {
});
ConnectionBean.useLocalCursor = MainSettingsManager.getShowVirtualMouse(this) || VMManager.isNeedUseVirtualMouse();
streamAudio = new StreamAudio(this);
streamAudio.setFile(AppConfig.vmFolder + Config.vmID + "/audio.raw");
}
private void setDefaulViewMode() {
@ -773,6 +779,8 @@ public class MainVNCActivity extends VncCanvasActivity {
binding.lnNosignal.setVisibility(View.GONE);
this.vncCanvas.setFocusableInTouchMode(true);
// syncCursorViewWithBitmap();
streamAudio.play();
});
}
@ -782,6 +790,8 @@ public class MainVNCActivity extends VncCanvasActivity {
isConnected = false;
binding.lnNosignal.setVisibility(View.VISIBLE);
if (started) isQMPPortOpening(firstConnection);
if (streamAudio.isPlaying()) streamAudio.stop();
});
}

View file

@ -142,6 +142,7 @@ public class ExportRomActivity extends AppCompatActivity {
vmConfigMap.put("bootFrom", current.bootFrom);
vmConfigMap.put("isShowBootMenu", current.isShowBootMenu);
vmConfigMap.put("isUseUefi", current.isUseUefi);
vmConfigMap.put("isUseLocalTime", current.isUseLocalTime);
vmConfigMap.put("qemu", current.itemExtra.replace(getRomPath, "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
vmConfigMap.put("arch", current.itemArch);
@ -186,7 +187,7 @@ public class ExportRomActivity extends AppCompatActivity {
} else if (_filelist.get(_repeat).endsWith("cqcm.json")) {
FileUtils.writeToFile(tempFolder, "cqcm.json", FileUtils.readAFile(_filelist.get(_repeat)).replace(getRomPath, "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
pathList.add(tempFolder + "cqcm.json");
} else if (_filelist.get(_repeat).endsWith("screenshot.ppm")) {
} else if (_filelist.get(_repeat).endsWith("screenshot.ppm") || _filelist.get(_repeat).endsWith("screenshot.png") || _filelist.get(_repeat).endsWith("audio.raw")) {
//ignore
} else {
pathList.add(_filelist.get(_repeat));

View file

@ -43,6 +43,7 @@ public class SplashActivity extends AppCompatActivity {
setupFolders();
MainSettingsManager.setOrientationSetting(this, 1);
setupFiles();
cleanUp();
runOnUiThread(this::finishSplash);
}).start();
}
@ -113,6 +114,10 @@ public class SplashActivity extends AppCompatActivity {
}
}
private void cleanUp() {
FileUtils.delete(AppConfig.vmFolder + "QuickRun");
}
private void finishSplash() {
if (MainSettingsManager.getShowLastCrashLog(this)) {
startActivity(new Intent(this, LastCrashActivity.class));

View file

@ -20,11 +20,11 @@ import java.util.Objects;
public class StartVM {
public static String cache;
public static String cdrompath;
private static boolean isUseUefi;
private static DataMainRoms vmConfigs;
public static String env(Activity activity, DataMainRoms vmData) {
vmConfigs = vmData;
if (VMManager.isNeedLoadMigrate() && FileUtils.isFileExists(AppConfig.vmFolder + Config.vmID + "/snapshot.sh")) {
String snapshotParams = FileUtils.readAFile(AppConfig.vmFolder + Config.vmID + "/snapshot.sh").replace("\n", "");
if (VMManager.isthiscommandsafe(snapshotParams, activity)) {
@ -50,12 +50,14 @@ public class StartVM {
}
extraParams = bootParams + extraParams;
cdrompath = vmData.imgCdrom;
isUseUefi = vmData.isUseUefi;
return env(activity, extraParams, vmData.itemPath, false);
}
public static String env(Activity activity, String extras, String img, boolean isQuickRun) {
if (isQuickRun) {
vmConfigs = new DataMainRoms();
vmConfigs.isUseLocalTime = false;
}
String filesDir = activity.getFilesDir().getAbsolutePath();
@ -107,7 +109,7 @@ public class StartVM {
params.add(hdd0);
}
if (cdrompath.isEmpty()) {
if (vmConfigs.imgCdrom.isEmpty()) {
File cdromFile = new File(filesDir + "/data/Vectras/drive.iso");
if (cdromFile.exists()) {
@ -143,16 +145,16 @@ public class StartVM {
cdrom += " -device";
cdrom += " usb-storage,bus=defaultxhci.0,drive=cdrom";
cdrom += " -drive";
cdrom += " if=none,id=cdrom,format=raw,media=cdrom,file='" + cdrompath + "'";
cdrom += " if=none,id=cdrom,format=raw,media=cdrom,file='" + vmConfigs.imgCdrom + "'";
} else {
if (ifType.isEmpty()) {
cdrom = "-cdrom";
cdrom += " '" + cdrompath + "'";
cdrom += " '" + vmConfigs.imgCdrom + "'";
} else {
cdrom = "-drive";
cdrom += " index=1";
cdrom += ",media=cdrom";
cdrom += ",file='" + cdrompath + "'";
cdrom += ",file='" + vmConfigs.imgCdrom + "'";
}
}
params.add(cdrom);
@ -215,7 +217,7 @@ public class StartVM {
bios += "file=" + AppConfig.basefiledir + "QEMU_EFI.img,format=raw,readonly=on,if=pflash";
bios += " -drive ";
bios += "file=" + AppConfig.basefiledir + "QEMU_VARS.img,format=raw,if=pflash";
} else if (MainSettingsManager.getArch(activity).equals("X86_64") && (MainSettingsManager.getuseUEFI(activity) || isUseUefi)) {
} else if (MainSettingsManager.getArch(activity).equals("X86_64") && (vmConfigs.isUseUefi)) {
bios = "-drive ";
bios += "file=" + AppConfig.basefiledir + "RELEASEX64_OVMF.fd,format=raw,readonly=on,if=pflash";
bios += " -drive ";
@ -243,7 +245,7 @@ public class StartVM {
}
if (MainSettingsManager.useLocalTime(activity)) {
if (vmConfigs.isUseLocalTime) {
params.add("-rtc");
params.add("base=localtime");
}
@ -284,8 +286,6 @@ public class StartVM {
params.add("defer");
}
isUseUefi = false;
return String.join(" ", params);
}
@ -374,7 +374,7 @@ public class StartVM {
if (!FileUtils.isFileExists(AppConfig.basefiledir + "QEMU_VARS.img"))
SetupFeatureCore.copyAssetToFile(context, "roms/QEMU_VARS.img", AppConfig.basefiledir + "QEMU_VARS.img");
} else if (arch.equals("X86_64") && (MainSettingsManager.getuseUEFI(context) || isUseUefi)) {
} else if (arch.equals("X86_64") && (MainSettingsManager.getuseUEFI(context) || vmConfigs.isUseUefi)) {
if (!FileUtils.isFileExists(AppConfig.basefiledir + "RELEASEX64_OVMF.fd"))
SetupFeatureCore.copyAssetToFile(context, "roms/RELEASEX64_OVMF.fd", AppConfig.basefiledir + "RELEASEX64_OVMF.fd");

View file

@ -52,6 +52,7 @@ import com.vectras.vm.main.core.MainStartVM;
import com.vectras.vm.main.vms.DataMainRoms;
import com.vectras.vm.manager.QemuConsoleDialog;
import com.vectras.vm.manager.QmpSender;
import com.vectras.vm.manager.VmActions;
import com.vectras.vm.settings.VNCSettingsActivity;
import com.vectras.vm.settings.X11DisplaySettingsActivity;
import com.vectras.vm.setupwizard.SetupFeatureCore;
@ -905,6 +906,7 @@ public class VMManager {
progressDialog.setFixTextColor(true);
progressDialog.show();
new Thread(() -> {
VmActions.takeScreenshot(_activity, false);
QmpSender.pause();
String migrateResult = startMigrate();
@ -1116,4 +1118,23 @@ public class VMManager {
}
return result;
}
public static String addAudioDevWav(String vmID, String env) {
final String audioDevParam = ",audiodev=snd0 -audiodev wav,id=snd0,path=" + AppConfig.vmFolder + vmID + "/audio.raw ";
String result = env;
if (env.startsWith("-device hda-duplex ") || env.contains(" -device hda-duplex ") || env.endsWith(" -device hda-duplex")) {
result = result.replaceFirst(" -device hda-duplex", " -device hda-duplex" + audioDevParam);
} else if (env.startsWith("-device cs4231a ") || env.contains(" -device cs4231a ") || env.endsWith(" -device cs4231a")) {
result = result.replaceFirst(" -device cs4231a", " -device cs4231a" + audioDevParam);
} else if (env.startsWith("-device ac97 ") || env.contains(" -device ac97 ") || env.endsWith(" -device ac97")) {
result = result.replaceFirst(" -device ac97", " -device ac97" + audioDevParam);
} else if (env.startsWith("-device es1370 ") || env.contains(" -device es1370 ") || env.endsWith(" -device es1370")) {
result = result.replaceFirst(" -device es1370", " -device es1370" + audioDevParam);
} else if (env.startsWith("-device sb16 ") || env.contains(" -device sb16 ") || env.endsWith(" -device sb16")) {
result = result.replaceFirst(" -device sb16", " -device sb16" + audioDevParam);
} else if (env.startsWith("-device adlib ") || env.contains(" -device adlib ") || env.endsWith(" -device adlib")) {
result = result.replaceFirst(" -device adlib", " -device adlib" + audioDevParam);
}
return result;
}
}

View file

@ -75,6 +75,7 @@ public class VMCreatorActivity extends AppCompatActivity {
private String vmID = VMManager.idGenerator();
private boolean isShowBootMenu = false;
private boolean isUseUefi = false;
private boolean isUseLocalTime = true;
private int bootFrom = 0;
@Override
@ -243,6 +244,9 @@ public class VMCreatorActivity extends AppCompatActivity {
binding.cbvShowbootmenu.setOnCheckedChangeListener((v, isChecked) -> isShowBootMenu = isChecked);
binding.cbvUseuefi.setOnCheckedChangeListener((v, isChecked) -> isUseUefi = isChecked);
binding.cbvUselocaltime.setOnCheckedChangeListener((v, isChecked) -> isUseLocalTime = isChecked);
if (!MainSettingsManager.getArch(this).equals("X86_64"))
binding.cbvUseuefi.setVisibility(View.GONE);
@ -583,6 +587,9 @@ public class VMCreatorActivity extends AppCompatActivity {
} else {
binding.cbvUseuefi.setVisibility(View.GONE);
}
isUseLocalTime = current.isUseLocalTime;
binding.cbvUselocaltime.setChecked(isUseLocalTime);
}
}
@ -715,6 +722,7 @@ public class VMCreatorActivity extends AppCompatActivity {
vmConfigMap.put("bootFrom", bootFrom);
vmConfigMap.put("isShowBootMenu", isShowBootMenu);
vmConfigMap.put("isUseUefi", isUseUefi);
vmConfigMap.put("isUseLocalTime", isUseLocalTime);
vmConfigMap.put("vmID", vmID);
vmConfigMap.put("qmpPort", 8080);
return vmConfigMap;

View file

@ -28,6 +28,7 @@ import com.vectras.vm.R;
import com.vectras.vm.VMManager;
import com.vectras.vm.logger.VectrasStatus;
import com.vectras.vm.manager.QmpSender;
import com.vectras.vm.manager.VmAudioManager;
import com.vectras.vm.settings.ExternalVNCSettingsActivity;
import com.vectras.vm.utils.DeviceUtils;
import com.vectras.vm.utils.DialogUtils;
@ -154,6 +155,7 @@ public class MainStartVM {
if (VMManager.isVMRunning(context, finalvmID)) {
Toast.makeText(context, "This VM is already running.", Toast.LENGTH_LONG).show();
DisplaySystem.launch(context);
VmAudioManager.stream(vmID);
return;
}
@ -193,11 +195,13 @@ public class MainStartVM {
return;
}
TextView vmBootNote = showProgressDialog(context, vmName, thumbnailFile);
FileUtils.delete(AppConfig.vmFolder + vmID + "/audio.raw");
TextView vmBootNote = showProgressDialog(context, vmName, thumbnailFile, vmID);
VMManager.isQemuStopedWithError = false;
String finalCommand = VMManager.addAudioDevSdl(String.format(runCommandFormat, env));
String finalCommand = VMManager.addAudioDevWav(vmID, String.format(runCommandFormat, env));
if (MainSettingsManager.getVmUi(context).equals("X11")) {
finalCommand = "export DISPLAY=:0 && " + finalCommand;
@ -239,7 +243,8 @@ public class MainStartVM {
if (!forceDisableMigrate && VMManager.isNeedLoadMigrate()) {
if (activity != null) {
activity.runOnUiThread(() -> {
if (vmBootNote != null ) vmBootNote.setText(activity.getString(R.string.resuming));
if (vmBootNote != null)
vmBootNote.setText(activity.getString(R.string.resuming));
});
}
String loadMigrateResponse = VMManager.loadMigrate();
@ -263,7 +268,8 @@ public class MainStartVM {
QmpSender.quickShutdown();
assert activity != null;
activity.runOnUiThread(() -> {
if (DialogUtils.isSafeDismiss(activity, progressDialog)) progressDialog.dismiss();
if (DialogUtils.isSafeDismiss(activity, progressDialog))
progressDialog.dismiss();
DialogUtils.threeDialog(
activity,
@ -314,7 +320,10 @@ public class MainStartVM {
// //This feature is not available yet.
} else if (MainSettingsManager.getVmUi(context).equals("X11") && !DisplaySystem.isUseBuiltInX11()) {
assert activity != null;
activity.runOnUiThread(() -> DisplaySystem.launch(context));
activity.runOnUiThread(() -> {
DisplaySystem.launch(context);
VmAudioManager.stream(vmID);
});
}
FileUtils.writeToFile(AppConfig.vmFolder + vmID, "snapshot.sh", env);
@ -323,13 +332,15 @@ public class MainStartVM {
if (activity != null) {
activity.runOnUiThread(() -> {
if (DialogUtils.isSafeDismiss(activity, progressDialog)) progressDialog.dismiss();
if (DialogUtils.isSafeDismiss(activity, progressDialog))
progressDialog.dismiss();
});
}
}).start();
} else {
assert activity != null;
if (DialogUtils.isSafeDismiss(activity, progressDialog)) progressDialog.dismiss();
if (DialogUtils.isSafeDismiss(activity, progressDialog))
progressDialog.dismiss();
}
skipIDEwithARM64DialogInStartVM = false;
@ -364,7 +375,7 @@ public class MainStartVM {
setDefault();
}
public static TextView showProgressDialog(Context context, String _content, String thumbnailFile) {
public static TextView showProgressDialog(Context context, String _content, String thumbnailFile, String vmID) {
View progressView = LayoutInflater.from(context).inflate(R.layout.dialog_start_vm, null);
TextView tvVMName = progressView.findViewById(R.id.vm_name);
TextView vmBootNote = progressView.findViewById(R.id.vm_boot_note);
@ -373,18 +384,20 @@ public class MainStartVM {
if (thumbnailFile != null) {
ImageView ivThumbnail = progressView.findViewById(R.id.iv_thumbnail);
if (thumbnailFile.isEmpty()) {
VMManager.setIconWithName(ivThumbnail, _content);
if (!thumbnailFile.isEmpty() && FileUtils.isFileExists(thumbnailFile)) {
Glide.with(context.getApplicationContext())
.load(new File(thumbnailFile))
.placeholder(R.drawable.ic_computer_180dp_with_padding)
.error(R.drawable.ic_computer_180dp_with_padding)
.into(ivThumbnail);
} else if (FileUtils.isFileExists(AppConfig.vmFolder + vmID + "/screenshot.png")) {
Glide.with(context.getApplicationContext())
.load(new File(AppConfig.vmFolder + vmID + "/screenshot.png"))
.placeholder(R.drawable.ic_computer_180dp_with_padding)
.error(R.drawable.ic_computer_180dp_with_padding)
.into(ivThumbnail);
} else {
if (FileUtils.isFileExists(thumbnailFile)) {
Glide.with(context.getApplicationContext())
.load(new File(thumbnailFile))
.placeholder(R.drawable.ic_computer_180dp_with_padding)
.error(R.drawable.ic_computer_180dp_with_padding)
.into(ivThumbnail);
} else {
VMManager.setIconWithName(ivThumbnail, _content);
}
VMManager.setIconWithName(ivThumbnail, _content);
}
}

View file

@ -10,6 +10,7 @@ import com.vectras.vm.R;
import com.vectras.vm.StartVM;
import com.vectras.vm.VMManager;
import com.vectras.vm.utils.DialogUtils;
import com.vectras.vm.utils.FileUtils;
import com.vectras.vterm.Terminal;
public class PendingCommand {
@ -56,10 +57,10 @@ public class PendingCommand {
} else {
Log.i(TAG, "Run VM...");
com.vectras.vm.StartVM.cdrompath = "";
Config.vmID = VMManager.idGenerator();
Config.vmID = "QuickRun/" + VMManager.idGenerator();
new Thread(() -> {
String env = StartVM.env(activity, command, "", true);
FileUtils.createDirectory(AppConfig.vmFolder + Config.vmID);
activity.runOnUiThread(() -> {
MainStartVM.startNow(activity, "Quick run", env, Config.vmID, null);
VMManager.lastQemuCommand = command;

View file

@ -52,4 +52,6 @@ public class DataMainRoms {
public boolean isShowBootMenu = false;
public boolean isUseUefi = false;
public boolean isUseLocalTime = true;
}

View file

@ -161,6 +161,11 @@ public class VmsFragment extends Fragment implements CallbackInterface.HomeCallT
} catch (JSONException ignored) {
romsMainData.isUseUefi = false;
}
try {
romsMainData.isUseLocalTime = json_data.getBoolean("isUseLocalTime");
} catch (JSONException ignored) {
romsMainData.isUseLocalTime = true;
}
romsMainData.itemExtra = json_data.getString("imgExtra");
tempdata.add(romsMainData);

View file

@ -16,6 +16,7 @@ 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;
@ -57,9 +58,7 @@ public class VmsHomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
final DataMainRoms current = data.get(position);
myHolder.textName.setText(current.itemName);
myHolder.textArch.setText(current.itemArch);
if (current.itemIcon.isEmpty()){
VMManager.setIconWithName(myHolder.ivIcon, current.itemName);
} else if (FileUtils.isFileExists(current.itemIcon)){
if (!current.itemIcon.isEmpty() && FileUtils.isFileExists(current.itemIcon)){
Glide.with(activity.getApplicationContext())
.load(new File(current.itemIcon))
.placeholder(R.drawable.ic_computer_180dp_with_padding)
@ -67,14 +66,21 @@ public class VmsHomeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(myHolder.ivIcon);
} else if (FileUtils.isFileExists(AppConfig.vmFolder + current.vmID + "/screenshot.png")) {
Glide.with(activity.getApplicationContext())
.load(new File(AppConfig.vmFolder + current.vmID + "/screenshot.png"))
.placeholder(R.drawable.ic_computer_180dp_with_padding)
.error(R.drawable.ic_computer_180dp_with_padding)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.into(myHolder.ivIcon);
} else {
myHolder.ivIcon.setImageResource(R.drawable.ic_computer_180dp_with_padding);
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);
StartVM.cdrompath = current.imgCdrom;
if (current.qmpPort == 0) {
Config.setDefault();
} else {

View file

@ -0,0 +1,42 @@
package com.vectras.vm.manager;
import android.content.Context;
import android.net.Uri;
import com.vectras.qemu.Config;
import com.vectras.vm.AppConfig;
import com.vectras.vm.utils.FileUtils;
import com.vectras.vm.utils.ImageUtils;
import java.io.File;
public class VmActions {
public static boolean takeScreenshot(Context context, boolean isSaveToGallery) {
boolean isSaved = QmpSender.takeScreenshot();
if (isSaved) {
try {
if (isSaveToGallery) {
Uri imageFile = ImageUtils.saveToGallery(
context,
ImageUtils.ppmToBitmap(new File(AppConfig.vmFolder + Config.vmID + "/screenshot.ppm")),
String.valueOf(System.currentTimeMillis())
);
FileUtils.copyFileFromUri(context, imageFile, AppConfig.vmFolder + Config.vmID + "/screenshot.png");
} else {
ImageUtils.saveBitmapToPNGFile(
ImageUtils.ppmToBitmap(new File(AppConfig.vmFolder + Config.vmID + "/screenshot.ppm")),
AppConfig.vmFolder + Config.vmID,
"screenshot.png"
);
}
} catch (Exception e) {
isSaved = false;
}
}
FileUtils.delete(AppConfig.vmFolder + Config.vmID + "/screenshot.ppm");
return isSaved;
}
}

View file

@ -0,0 +1,30 @@
package com.vectras.vm.manager;
import android.util.Log;
import com.vectras.qemu.Config;
import com.vectras.vm.AppConfig;
import com.vectras.vm.VectrasApp;
import com.vectras.vm.utils.FileUtils;
import com.vectras.vm.utils.StreamAudio;
public class VmAudioManager {
private static final String TAG = "VmAudioManager";
public static final StreamAudio streamAudio = new StreamAudio(VectrasApp.getContext());
public static void stream(String vmID) {
if (streamAudio.isPlaying()) streamAudio.stop();
streamAudio.setFile(AppConfig.vmFolder + vmID + "/audio.raw");
streamAudio.play();
new Thread(() -> {
while (FileUtils.isFileExists(Config.getLocalQMPSocketPath(vmID))) {
try { Thread.sleep(2000); } catch (InterruptedException ignored) { return; }
}
Log.d(TAG, "Stoped.");
streamAudio.stop();
}).start();
}
}

View file

@ -3,6 +3,7 @@ package com.vectras.vm.manager;
import android.androidVNC.ConnectionBean;
import android.app.Dialog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@ -228,26 +229,12 @@ public class VmControllerDialog extends DialogFragment {
progressDialog.show();
new Thread(() -> {
boolean isSaved = QmpSender.takeScreenshot();
if (isSaved) {
try {
ImageUtils.saveToGallery(
requireActivity(),
ImageUtils.ppmToBitmap(new File (AppConfig.vmFolder + Config.vmID + "/screenshot.ppm")),
String.valueOf(System.currentTimeMillis())
);
} catch (Exception e) {
isSaved = false;
}
}
boolean isSaved = VmActions.takeScreenshot(requireActivity(), true);
FileUtils.delete(AppConfig.vmFolder + Config.vmID + "/screenshot.ppm");
boolean finalIsSaved = isSaved;
new Handler(Looper.getMainLooper()).post(() -> {
if (!isAdded()) return;
progressDialog.reset();
Toast.makeText(requireActivity().getApplicationContext(), getString(finalIsSaved ? R.string.saved_to_the_gallery : R.string.unable_to_take_a_screenshot), Toast.LENGTH_SHORT).show();
Toast.makeText(requireActivity().getApplicationContext(), getString(isSaved ? R.string.saved_to_the_gallery : R.string.unable_to_take_a_screenshot), Toast.LENGTH_SHORT).show();
dismiss();
});
}).start();

View file

@ -8,6 +8,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.util.Log;
import android.view.View;
import androidx.activity.EdgeToEdge;
@ -24,10 +25,13 @@ import com.vectras.vm.utils.PackageUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Locale;
import java.util.Objects;
public class UpdaterActivity extends AppCompatActivity {
private final String TAG ="UpdaterActivity";
ActivityUpdaterBinding binding;
String downloadUrl = "";
@ -78,7 +82,7 @@ public class UpdaterActivity extends AppCompatActivity {
int versionCode = PackageUtils.getThisVersionCode(getApplicationContext());
String versionName = PackageUtils.getThisVersionName(getApplicationContext());
Retrofit2Utils.get(AppConfig.vectrasRaw + "software-store.json", ((isSuccess, body, status, error) -> {
Retrofit2Utils.get(AppConfig.vectrasRaw + "UpdateConfig.json", ((isSuccess, body, status, error) -> {
if (isSuccess) {
binding.lpiProgressbar.setVisibility(View.GONE);
binding.lnBottombar.setVisibility(View.VISIBLE);
@ -140,12 +144,15 @@ public class UpdaterActivity extends AppCompatActivity {
null);
}
} catch (JSONException e) {
Log.d(TAG, "JSONException: ", e);
whenUpToDate();
}
} else {
Log.d(TAG, "Empty body.");
whenUpToDate();
}
} else {
Log.d(TAG, "Failed.");
whenUpToDate();
}
}));

View file

@ -5,7 +5,6 @@ import android.content.Context;
import android.os.Build;
import android.util.Log;
import com.vectras.vm.AppConfig;
import com.vectras.vm.R;
import com.vectras.vm.VMManager;
import com.vectras.vm.utils.DeviceUtils;
@ -60,13 +59,30 @@ public class SetupFeatureCore {
lastErrorLog = "";
File distroDir = new File(filesDir + "/distro");
if (!distroDir.exists()) {
if (!distroDir.mkdir()) {
lastErrorLog = "Failed to create directory: " + filesDir + "/distro";
return false;
}
}
File binDir = new File(distroDir + "/bin");
if (!binDir.exists()) {
if (!isInstalledProot(context)) {
if (!extractSystemFiles(context, "bootstrap", "")) return false;
if (!extractSystemFiles(context, "bootstrap", "", false)) {
if (!extractSystemFiles(context, "bootstrap", "", true)) return false;
}
/*if (!mkSymlinks(filesDir + "/usr/lib/")) {
lastErrorLog = "Failed to create symlinks.";
return false;
}*/
}
if (isInstalledDistro(context)) return true;
if (isInstalledDistro(context)) {
lastErrorLog = "Installed proot.";
return true;
}
File tmpDir = new File(context.getFilesDir(), "usr/tmp");
if (!tmpDir.isDirectory()) {
@ -77,12 +93,17 @@ public class SetupFeatureCore {
}
}
return extractSystemFiles(context, "alpine19", "distro");
if (!extractSystemFiles(context, "alpine19", "distro", false)) {
return extractSystemFiles(context, "alpine19", "distro", true);
} else {
return true;
}
}
return false;
}
public static boolean extractSystemFiles(Context context, String fromAsset, String extractTo) {
public static boolean extractSystemFiles(Context context, String fromAsset, String extractTo, boolean tryNoSameOwner) {
String randomFileName = VMManager.startRamdomVMID();
String filesDir = context.getFilesDir().getAbsolutePath();
String abi = Build.SUPPORTED_ABIS[0];
@ -99,9 +120,11 @@ public class SetupFeatureCore {
// Step 2: Run tar extraction
if (isCompleted) {
String[] cmdline = {"tar", "xf", extractedFilePath, "-C", filesDir + "/" + extractTo};
String[] cmdline2 = {"tar", "xf", extractedFilePath, "-C", filesDir + "/" + extractTo, "--no-same-owner"};
Process process = null;
try {
process = Runtime.getRuntime().exec(cmdline);
process = Runtime.getRuntime().exec(tryNoSameOwner ? cmdline2 : cmdline);
// Capture standard error output (stderr)
BufferedReader errorReader =
@ -121,11 +144,18 @@ public class SetupFeatureCore {
}
// If there was any output in stderr, treat it as an error
return exitCode == 0 && errorOutput.length() <= 0;
if (exitCode == 0 && errorOutput.length() <= 0) {
return true;
} else {
lastErrorLog = errorOutput.toString();
return false;
}
} catch (IOException | InterruptedException e) {
lastErrorLog = lastErrorLog.isEmpty() ? e.toString() : lastErrorLog + "\n" + e;
Log.e(TAG, "extractSystemFiles: ", e);
} finally {
FileUtils.delete(extractedFilePath);
if (process != null) {
process.destroy();
}
@ -134,6 +164,27 @@ public class SetupFeatureCore {
return false;
}
public static boolean mkSymlinks(String libPath) {
Log.d(TAG, "mkSymlinks: Creating...");
return FileUtils.symlink(libPath + "libtalloc.so.2.4.2", libPath + "libtalloc.so.2") &&
FileUtils.symlink(libPath + "libtalloc.so.2", libPath + "libtalloc.so");
}
public static void fixPermissions(String distroPath) {
File binDir = new File(distroPath + "/bin");
File usrBinDir = new File(distroPath + "/usr/bin");
File sbinDir = new File(distroPath + "/sbin");
File usrSbinDir = new File(distroPath + "/usr/sbin");
for (File dir : new File[]{binDir, usrBinDir, sbinDir, usrSbinDir}) {
if (dir.exists() && dir.listFiles() != null) {
for (File f : dir.listFiles()) {
f.setExecutable(true, false);
}
}
}
}
public static boolean copyAssetToFile(Context context, String assetPath, String outputPath) {
try (InputStream in = context.getAssets().open(assetPath);
OutputStream out = new FileOutputStream(outputPath)) {

View file

@ -856,6 +856,26 @@ public class FileUtils {
}
}
public static boolean symlink(String targetPath, String linkPath) {
File target = new File(targetPath);
File linkFile = new File(linkPath);
if (target.exists()) {
if (linkFile.exists() && !linkFile.delete()) return false;
try {
Os.symlink(target.getAbsolutePath(), linkFile.getAbsolutePath());
Log.d(TAG, "Symlink: " + linkFile.getAbsolutePath() + "" + target.getAbsolutePath());
} catch (ErrnoException e) {
Log.e(TAG, "Symlink failed: " + e.getMessage());
return false;
}
} else {
return false;
}
return true;
}
public static String getMd5(String filePath) {
try (InputStream inputStream = new FileInputStream(filePath)) {
byte[] buffer = new byte[1024];

View file

@ -0,0 +1,110 @@
package com.vectras.vm.utils;
import android.app.Activity;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioTrack;
import android.util.Log;
import java.io.RandomAccessFile;
public class StreamAudio {
private final String TAG = "StreamAudio";
private Context context;
private boolean isPlay;
private String filePath = "";
public StreamAudio(Context context) {
this.context = context;
}
public boolean isPlaying() {
return isPlay;
}
public void stop() {
isPlay = false;
}
public void play() {
if (!filePath.isEmpty()) streamFromFile();
}
public void setFile(String path) {
filePath = path;
}
public void streamFromFile() {
isPlay = true;
new Thread(() -> {
Log.d(TAG, "Play: " + filePath);
while (isPlay && !FileUtils.isFileExists(filePath)) {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
isPlay = false;
Log.d(TAG, "Failed to wait for: " + filePath);
return;
}
}
if (!isPlay) return;
Log.d(TAG, "Preparing to play: " + filePath);
int minBuf = AudioTrack.getMinBufferSize(44100,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.ENCODING_PCM_16BIT);
AudioTrack audioTrack = new AudioTrack.Builder()
.setAudioAttributes(new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.build())
.setAudioFormat(new AudioFormat.Builder()
.setSampleRate(44100)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.build())
.setBufferSizeInBytes(minBuf * 4)
.setTransferMode(AudioTrack.MODE_STREAM)
.build();
audioTrack.play();
try (RandomAccessFile raf = new RandomAccessFile(filePath, "r")) {
byte[] buffer = new byte[minBuf];
long lastPos = raf.length();
Log.d(TAG, "Playing: " + filePath);
while (isPlay && !isContextDestroyed(context)) {
long fileSize = raf.length();
if (fileSize > lastPos) {
raf.seek(lastPos);
int bytesRead = raf.read(buffer);
if (bytesRead > 0) {
audioTrack.write(buffer, 0, bytesRead);
lastPos += bytesRead;
}
} else {
Thread.sleep(10);
}
}
} catch (Exception e) {
isPlay = false;
Log.e(TAG, e.getMessage());
}
}).start();
}
private boolean isContextDestroyed(Context context) {
if (context instanceof Activity activity) {
return activity.isDestroyed() || activity.isFinishing();
}
return false;
}
}

View file

@ -285,6 +285,13 @@
android:layout_height="wrap_content"
app:setText="@string/use_uefi_for_x64_only"/>
<com.vectras.vm.view.CheckBoxView
android:id="@+id/cbv_uselocaltime"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:setText="@string/use_local_time"
app:setChecked="true"/>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View file

@ -57,7 +57,7 @@
android:key="ifType"
app:useSimpleSummaryProvider="true"
app:icon="@drawable/round_dns_24" />
<ListPreference
<!--<ListPreference
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:key="boot"
@ -66,7 +66,7 @@
android:entryValues="@array/bootValues"
android:defaultValue="c"
app:icon="@drawable/ic_b"
app:useSimpleSummaryProvider="true" />
app:useSimpleSummaryProvider="true" />-->
<SwitchPreferenceCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -102,13 +102,13 @@
android:key="useDefaultBios"
android:title="@string/use_default_bios_uefi"
app:icon="@drawable/hard_disk_24px" />
<SwitchPreferenceCompat
<!--<SwitchPreferenceCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:defaultValue="false"
android:key="useUEFI"
android:title="@string/use_uefi_for_x64_only"
app:icon="@drawable/hard_disk_24px" />
app:icon="@drawable/hard_disk_24px" />-->
<SwitchPreferenceCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -116,13 +116,13 @@
android:key="useMemoryOvercommit"
android:title="@string/use_memory_overcommit"
app:icon="@drawable/memory_alt_24px" />
<SwitchPreferenceCompat
<!--<SwitchPreferenceCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:defaultValue="true"
android:key="useLocalTime"
android:title="@string/use_local_time"
app:icon="@drawable/schedule_24px" />
app:icon="@drawable/schedule_24px" />-->
</PreferenceCategory>
<PreferenceCategory
android:layout_width="wrap_content"

View file

@ -5,11 +5,11 @@
"url": "https://github.com/xoureldeen/Vectras-VM-Android/releases",
"Message": "<h2>3.9.0</h2>\nBugs fixed.",
"cancellable": true,
"versionCodeBeta":"101",
"versionNameBeta":"3.9.7",
"versionNameBetas":"3.0.0,3.1.0,3.2.1,3.2.2,3.2.3,3.2.4,3.2.5,3.2.6,3.2.7,3.2.8,3.2.9,3.2.10,3.3.1,3.3.2,3.3.3,3.3.4,3.3.5,3.3.6,3.3.7,3.3.8,3.3.9,3.4.1,3.4.2,3.4.3,3.4.4,3.4.5,3.4.6,3.4.7,3.4.8,3.4.9,3.5.1,3.5.2,3.5.3,3.5.4,3.5.5,3.5.6,3.5.7,3.5.8,3.5.9,3.6.1,3.6.2,3.6.3,3.6.4,3.6.5,3.6.6,3.6.7,3.6.8,3.6.9,3.7.1,3.7.2,3.7.3,3.7.4,3.7.5,3.7.6,3.7.7,3.7.8,3.7.9,3.8.0,3.8.1,3.8.2,3.8.3,3.8.4,3.8.5,3.8.6,3.8.7,3.8.8,3.8.9,3.9.0,3.9.1,3.9.2,3.9.3,3.9.4,3.9.5,3.9.6,3.9.7",
"versionCodeBeta":"102",
"versionNameBeta":"3.9.8",
"versionNameBetas":"3.0.0,3.1.0,3.2.1,3.2.2,3.2.3,3.2.4,3.2.5,3.2.6,3.2.7,3.2.8,3.2.9,3.2.10,3.3.1,3.3.2,3.3.3,3.3.4,3.3.5,3.3.6,3.3.7,3.3.8,3.3.9,3.4.1,3.4.2,3.4.3,3.4.4,3.4.5,3.4.6,3.4.7,3.4.8,3.4.9,3.5.1,3.5.2,3.5.3,3.5.4,3.5.5,3.5.6,3.5.7,3.5.8,3.5.9,3.6.1,3.6.2,3.6.3,3.6.4,3.6.5,3.6.6,3.6.7,3.6.8,3.6.9,3.7.1,3.7.2,3.7.3,3.7.4,3.7.5,3.7.6,3.7.7,3.7.8,3.7.9,3.8.0,3.8.1,3.8.2,3.8.3,3.8.4,3.8.5,3.8.6,3.8.7,3.8.8,3.8.9,3.9.0,3.9.1,3.9.2,3.9.3,3.9.4,3.9.5,3.9.6,3.9.7,3.9.8",
"sizeBeta": "45 MB",
"urlBeta": "https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases",
"MessageBeta": "<h2>3.9.7</h2>Bugs fixed.",
"MessageBeta": "<h2>3.9.8</h2>Bugs fixed.",
"cancellableBeta": true
}