- Fixed issues in Store categories.
- Fixed issue where custom memory was set too large.
- Improved ROM error detection.
- Updated the selection dialog.
- Added export and import support for paused virtual machines.
- Improved virtual machine resuming.
- Fixed crash when folders could not be opened.
- Improved virtual machine list data error detection.
- Fixed issue when selecting the Custom option in the Setup Wizard.
- Fixed crash when virtual machine pause fails.
This commit is contained in:
An Bui 2026-03-27 20:29:53 +07:00
parent e0deccfc8d
commit 2ecf30929a
36 changed files with 501 additions and 171 deletions

View file

@ -87,7 +87,7 @@ QEMU 9.2.2 - 3dfx (recommended and for Vectras VM 3.5.1+):
- [For Android ARM (32-bit)](https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases/download/3.5.4/base-vectras-vm-armeabi-v7a.tar.gz)
- [For Android x86 (64-bit)](https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases/download/3.5.1/base-generic-vectras-vm-x86_64.tar.gz)
- [For Android x86 (32-bit)](https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases/download/3.5.4/base-vectras-vm-x86.tar.gz)
- [See them at the Internet Archive (64-bit)](https://archive.org/details/vectras-vm-x86_64.tar_202603nbab)
- [See them at the Internet Archive (64-bit)](https://archive.org/details/qemu-9-2-2-3dfx-for-vectras-vm-nbab)
- [See them at the Internet Archive (32-bit)](https://archive.org/details/qemu-9-2-2-for-vectras-vm-nbab)
QEMU 9.2.2 - 3dfx (for Vectras VM 3.2.9 - 3.4.9):

View file

@ -12,8 +12,8 @@ android {
applicationId "com.vectras.vm"
minSdk minApi
targetSdk targetApi
versionCode 93
versionName "3.8.9"
versionCode 94
versionName "3.9.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
multiDexEnabled true
@ -78,48 +78,41 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.7.1'
implementation 'com.google.android.material:material:1.14.0-alpha10'
implementation "androidx.annotation:annotation:1.9.1"
implementation "androidx.core:core-ktx:1.18.0"
implementation "androidx.drawerlayout:drawerlayout:1.2.0"
implementation "androidx.preference:preference-ktx:1.2.1"
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0"
implementation "androidx.viewpager:viewpager:1.1.0"
implementation platform('com.google.firebase:firebase-bom:34.11.0')
implementation 'com.google.firebase:firebase-analytics'
// implementation("com.google.firebase:firebase-auth")
// implementation("com.google.firebase:firebase-database")
// implementation 'com.google.firebase:firebase-storage'
// implementation("com.google.android.gms:play-services-auth:21.4.0")
implementation("com.google.firebase:firebase-crashlytics")
// implementation 'com.google.android.gms:play-services-ads:24.6.0'
implementation 'com.google.firebase:firebase-messaging:25.0.1'
implementation 'com.google.guava:guava:33.5.0-jre'
implementation 'com.google.code.gson:gson:2.13.2'
implementation "androidx.window:window:1.5.1"
implementation "commons-io:commons-io:2.21.0"
implementation 'com.airbnb.android:lottie:6.7.1'
implementation 'org.apache.commons:commons-compress:1.28.0'
// implementation 'com.google.firebase:firebase-crashlytics-buildtools:3.0.6'
implementation 'androidx.activity:activity-ktx:1.13.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.documentfile:documentfile:1.1.0"
implementation 'androidx.core:core-ktx:1.18.0'
implementation libs.androidx.appcompat
implementation libs.material
implementation libs.androidx.annotation
implementation libs.androidx.core.ktx
implementation libs.androidx.drawerlayout
implementation libs.androidx.preference.ktx
implementation libs.androidx.swiperefreshlayout
implementation libs.androidx.viewpager
implementation platform(libs.firebase.bom)
implementation libs.firebase.analytics
implementation libs.firebase.messaging
implementation libs.guava
implementation libs.gson
implementation libs.androidx.window
implementation libs.commons.io
implementation libs.lottie
implementation libs.commons.compress
implementation libs.androidx.activity.ktx
implementation libs.androidx.constraintlayout
implementation libs.androidx.preference.ktx
implementation libs.androidx.documentfile
implementation libs.androidx.core.ktx
compileOnly project(':shell-loader:stub')
implementation project(":terminal-view")
// Retrofit
implementation 'com.squareup.retrofit2:retrofit:3.0.0'
implementation 'com.squareup.retrofit2:converter-gson:3.0.0'
implementation libs.retrofit
implementation libs.converter.gson
// Glide
implementation 'com.github.bumptech.glide:glide:5.0.5'
annotationProcessor 'com.github.bumptech.glide:compiler:5.0.5'
implementation libs.glide
annotationProcessor libs.compiler
// Test dependencies
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.7.0'
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core
}

View file

@ -258,7 +258,7 @@
</provider>
<service
android:name=".FCMService"
android:name=".fcm.FCMService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />

View file

@ -2,6 +2,8 @@ package com.anbui.elephant.interaction;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Looper;
import androidx.preference.PreferenceManager;
@ -23,6 +25,9 @@ public class Interaction {
private final String contentId;
private DataInteraction dataInteraction;
private final SharedPreferences sharedPreferences;
public boolean isRequesting;
public boolean isAllowAction;
private Runnable waitingAction;
public interface InteractionCallback {
void onResult(boolean isSuccess, int views, int likes);
@ -44,19 +49,32 @@ public class Interaction {
return;
}
get(((isSuccess, views, likes) -> view(callback)));
get(((isSuccess, views, likes) -> {
view(callback);
isAllowAction = true;
}));
LogPrinter.print(TAG, "Initialized.");
}
public void get(InteractionCallback callback) {
if (isRequesting) {
LogPrinter.print(TAG, "Busy, there's another connection in view.");
return;
}
if (!isReady()) {
callback.onResult(false, 1, 0);
LogPrinter.print(TAG, "Not ready in get.");
return;
}
isRequesting = true;
Retrofit2Utils.get(GET_URL, ((isSuccess, body, status, error) -> {
isRequesting = false;
isAllowAction = true;
if (isSuccess && JSONUtils.isValidFromString(body)) {
dataInteraction = new Gson().fromJson(body, DataInteraction.class);
callback.onResult(true, dataInteraction.views, dataInteraction.likes);
@ -71,6 +89,12 @@ public class Interaction {
private boolean isTryingView;
public void view(InteractionCallback callback) {
if (isRequesting || !isAllowAction) {
if (isAllowAction) waitingAction = () -> view(callback);
LogPrinter.print(TAG, "Busy, there's another connection, or the action was blocked due to the action being performed too quickly in view.");
return;
}
if (isViewed()) {
callback.onResult(true, dataInteraction != null ? dataInteraction.views : 1, dataInteraction != null ? dataInteraction.likes : 0);
LogPrinter.print(TAG, "Viewed.");
@ -95,7 +119,11 @@ public class Interaction {
+ "\"token\":" + "\"" + dataInteraction.token + "\""
+ "}";
isRequesting = true;
Retrofit2Utils.post(VIEW_URL, jsonRaw, ((isSuccess, body, status, error) -> {
isRequesting = false;
if (isNeedRetry(status) && !isTryingView) {
isTryingView = true;
get((success, views, likes) -> view(callback));
@ -115,12 +143,26 @@ public class Interaction {
callback.onResult(false, 1, 0);
LogPrinter.print(TAG, "View unsucceed.");
}
if (waitingAction != null) {
waitingAction.run();
waitingAction = null;
}
isAllowAction = false;
new Handler(Looper.getMainLooper()).postDelayed(() -> isAllowAction = true, 1000);
}));
}
private boolean isTryingLike;
public void like(InteractionCallback callback) {
if (isRequesting || !isAllowAction) {
if (isAllowAction) waitingAction = () -> like(callback);
LogPrinter.print(TAG, "Busy, there's another connection, or the action was blocked due to the action being performed too quickly in like.");
return;
}
if (!isReadyToPost() && !isTryingLike) {
isTryingLike = true;
get((success, views, likes) -> like(callback));
@ -140,7 +182,11 @@ public class Interaction {
+ "\"token\":" + "\"" + dataInteraction.token + "\""
+ "}";
isRequesting = true;
Retrofit2Utils.post(LIKE_URL, jsonRaw, ((isSuccess, body, status, error) -> {
isRequesting = false;
if (isNeedRetry(status) && !isTryingLike) {
isTryingLike = true;
get((success, views, likes) -> like(callback));
@ -160,6 +206,14 @@ public class Interaction {
callback.onResult(false, 1, 0);
LogPrinter.print(TAG, "Like unsucceed.");
}
if (waitingAction != null) {
waitingAction.run();
waitingAction = null;
}
isAllowAction = false;
new Handler(Looper.getMainLooper()).postDelayed(() -> isAllowAction = true, 1000);
}));
}
@ -172,11 +226,11 @@ public class Interaction {
}
public int getViewCount() {
return dataInteraction.views;
return dataInteraction != null ? dataInteraction.views : 1;
}
public int getLikeCount() {
return dataInteraction.likes;
return dataInteraction != null ? dataInteraction.likes : (isLiked() ? 1 : 0);
}
private boolean isReady() {

View file

@ -11,8 +11,6 @@ import com.vectras.qemu.MainSettingsManager;
import com.vectras.vm.utils.TextUtils;
public class RamInfo {
public static Activity activity;
public static int safeLongToInt(long l) {
if (l < Integer.MIN_VALUE || l > Integer.MAX_VALUE) {
throw new IllegalArgumentException(l + " cannot be cast to int without changing its value.");
@ -31,6 +29,11 @@ public class RamInfo {
int totalRamInt = safeLongToInt(totalMem);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(activity);
if (prefs.getBoolean("customMemory", false) && TextUtils.isNumberOnly(prefs.getString("memory", String.valueOf(256)))) {
if (Long.parseLong(prefs.getString("memory", String.valueOf(256))) > totalMem) {
prefs.edit().putInt("memory", totalRamInt / 2).apply();
return totalRamInt / 2;
}
return Integer.parseInt(prefs.getString("memory", String.valueOf(256)));
} else {
return freeRamInt - 100;

View file

@ -141,7 +141,7 @@ public class ExportRomActivity extends AppCompatActivity {
vmConfigMap.put("bootFrom", current.bootFrom);
vmConfigMap.put("isShowBootMenu", current.isShowBootMenu);
vmConfigMap.put("qemu", current.itemExtra.replace(AppConfig.vmFolder + current.vmID + "/", "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
vmConfigMap.put("qemu", current.itemExtra.replace(getRomPath, "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
vmConfigMap.put("arch", current.itemArch);
if (Objects.requireNonNull(binding.edAuthor.getText()).toString().isEmpty()) {
@ -173,8 +173,14 @@ public class ExportRomActivity extends AppCompatActivity {
if (_filelist.get(_repeat).endsWith("rom-data.json")) {
filePaths[filePaths.length - 1] = tempFolder + "rom-data.json";
} else if (_filelist.get(_repeat).endsWith("snapshot.sh")) {
String snapshotParams = FileUtils.readAFile(_filelist.get(_repeat));
snapshotParams = StartVM.removeQmpParams(snapshotParams);
snapshotParams = StartVM.removeDisplayParams(snapshotParams);
FileUtils.writeToFile(tempFolder, "snapshot.sh", snapshotParams.replace(getRomPath, "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
filePaths[filePaths.length - 1] = tempFolder + "snapshot.sh";
} else if (_filelist.get(_repeat).endsWith("cqcm.json")) {
FileUtils.writeToFile(tempFolder, "cqcm.json", FileUtils.readAFile(_filelist.get(_repeat)).replace(AppConfig.vmFolder + current.vmID + "/", "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
FileUtils.writeToFile(tempFolder, "cqcm.json", FileUtils.readAFile(_filelist.get(_repeat)).replace(getRomPath, "OhnoIjustrealizeditsmidnightandIstillhavetodothis"));
filePaths[filePaths.length - 1] = tempFolder + "cqcm.json";
} else {
filePaths[filePaths.length - 1] = _filelist.get(_repeat);

View file

@ -1,40 +0,0 @@
package com.vectras.vm;
import androidx.annotation.NonNull;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.vectras.vm.utils.NotificationUtils;
import java.util.Map;
public class FCMService
extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
if (message.getNotification() != null) {
String title = message.getNotification().getTitle();
String body = message.getNotification().getBody();
String image = message.getNotification().getImageUrl() != null ? message.getNotification().getImageUrl().toString() : null;
Map<String, String> data = message.getData();
NotificationUtils.pushNow(this,
1,
NotificationUtils.generalChannelId,
title,
body,
R.drawable.ic_vectras_vm_48,
image,
-1,
data.get("url"),
null);
}
}
@Override
public void onNewToken(@NonNull String token) {
}
}

View file

@ -335,6 +335,8 @@ public class RomInfo extends AppCompatActivity {
}
private void sendLikeUpdate() {
if (interaction.isRequesting || !interaction.isAllowAction) return;
binding.btnLike.setIcon(ContextCompat.getDrawable(RomInfo.this, !interaction.isLiked() ? R.drawable.thumb_up_filled_24px : R.drawable.thumb_up_24px));
binding.btnLike.setText(!interaction.isLiked() ? getString(R.string.liked) : getString(R.string.like));

View file

@ -1,6 +1,8 @@
package com.vectras.vm;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import com.vectras.qemu.Config;
import com.vectras.qemu.MainSettingsManager;
@ -8,6 +10,7 @@ import com.vectras.qemu.utils.RamInfo;
import com.vectras.vm.creator.VMCreatorSelector;
import com.vectras.vm.main.vms.DataMainRoms;
import com.vectras.vm.utils.FileUtils;
import com.vectras.vm.utils.TextUtils;
import java.io.File;
import java.util.ArrayList;
@ -20,6 +23,21 @@ public class StartVM {
public static String cdrompath;
public static String env(Activity activity, DataMainRoms 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)) {
snapshotParams = removeQemuSystem(snapshotParams);
snapshotParams = removeQmpParams(snapshotParams);
snapshotParams = removeDisplayParams(snapshotParams);
snapshotParams = getQmpParams() + " " + snapshotParams;
snapshotParams = getQemuSystem(activity) + " " + snapshotParams;
snapshotParams += " " + getDisplayParams(activity);
if (!snapshotParams.contains("-incoming defer")) snapshotParams += " -incoming defer";
Log.d("StartVM.env", snapshotParams);
return snapshotParams;
}
}
String extraParams = vmData.itemExtra;
String bootFromParams = Objects.requireNonNull(VMCreatorSelector.getBootFrom(activity, vmData.bootFrom).get("value")).toString();
@ -56,8 +74,7 @@ public class StartVM {
else if (MainSettingsManager.getArch(activity).equals("PPC"))
params.add("qemu-system-ppc");
params.add("-qmp");
params.add("unix:" + Config.getLocalQMPSocketPath() + ",server,nowait");
params.add(getQmpParams());
String ifType;
ifType= MainSettingsManager.getIfType(activity);
@ -252,47 +269,10 @@ public class StartVM {
params.add(finalextra);
if (isQuickRun) {
params.add("-qmp");
params.add("unix:" + Config.getLocalQMPSocketPath() + ",server,nowait");
params.add(getQmpParams());
}
if (MainSettingsManager.getVmUi(activity).equals("VNC")) {
if (!MainSettingsManager.getVncExternalPassword(activity).isEmpty()) {
params.add("-object ");
params.add("secret,id=vncpass,data=\"" + MainSettingsManager.getVncExternalPassword(activity) + "\"");
}
String vncStr = "-vnc ";
params.add(vncStr);
// Allow connections only from localhost using localsocket without a password
if (MainSettingsManager.getVncExternal(activity)) {
String vncParams = Config.defaultVNCHost + ":" + Config.defaultVNCPort;
if (!MainSettingsManager.getVncExternalPassword(activity).isEmpty()) vncParams += ",password-secret=vncpass";
params.add(vncParams);
} else {
String vncSocketParams = "unix:";
vncSocketParams += Config.getLocalVNCSocketPath();
params.add(vncSocketParams);
}
//if (!MainSettingsManager.getArch(activity).equals("PPC") || !MainSettingsManager.getArch(activity).equals("ARM64")) {
params.add("-monitor");
params.add("vc");
//}
} else if (MainSettingsManager.getVmUi(activity).equals("SPICE")) {
String spiceStr = "-spice ";
spiceStr += "port=6999,disable-ticketing=on";
params.add(spiceStr);
} else if (MainSettingsManager.getVmUi(activity).equals("X11")) {
params.add("-display");
params.add(MainSettingsManager.getUseSdl(activity) ? "sdl" : "gtk" + ",gl=on");
params.add("-monitor");
params.add(MainSettingsManager.getRunQemuWithXterm(activity) ? "stdio" : "vc");
}
params.add(getDisplayParams(activity));
if (VMManager.isNeedLoadMigrate()) {
params.add("-incoming");
@ -302,4 +282,77 @@ public class StartVM {
return String.join(" ", params);
}
public static String getQemuSystem(Context context) {
if (MainSettingsManager.getArch(context).equals("I386"))
return "qemu-system-i386";
else if (MainSettingsManager.getArch(context).equals("X86_64"))
return "qemu-system-x86_64";
else if (MainSettingsManager.getArch(context).equals("ARM64"))
return "qemu-system-aarch64";
else if (MainSettingsManager.getArch(context).equals("PPC"))
return "qemu-system-ppc";
return "qemu-system-x86_64";
}
public static String removeQemuSystem(String params) {
return params.replaceAll("^qemu-system-\\S+\\s*", "");
}
public static String getQmpParams() {
return "-qmp unix:" + Config.getLocalQMPSocketPath() + ",server,nowait";
}
public static String removeQmpParams(String params) {
return params.replaceAll("(?<=\\s|^)-qmp\\s+(\"[^\"]+\"|\\S+)", "");
}
public static String getDisplayParams(Context context) {
String params = "";
if (MainSettingsManager.getVmUi(context).equals("VNC")) {
if (!MainSettingsManager.getVncExternalPassword(context).isEmpty()) {
params += "-object secret,id=vncpass,data=\"" + MainSettingsManager.getVncExternalPassword(context) + "\"";
}
params += (params.isEmpty() ? "" : " ") + "-vnc ";
// Allow connections only from localhost using localsocket without a password
if (MainSettingsManager.getVncExternal(context)) {
String vncParams = Config.defaultVNCHost + ":" + Config.defaultVNCPort;
if (!MainSettingsManager.getVncExternalPassword(context).isEmpty())
vncParams += ",password-secret=vncpass";
params += vncParams;
} else {
params += "unix:" + Config.getLocalVNCSocketPath();
}
params += " -monitor vc";
} else if (MainSettingsManager.getVmUi(context).equals("SPICE")) {
params += "-spice port=6999,disable-ticketing=on";
} else if (MainSettingsManager.getVmUi(context).equals("X11")) {
params += "-display ";
params += MainSettingsManager.getUseSdl(context) ? "sdl" : "gtk" + ",gl=on";
params += " -monitor ";
params += MainSettingsManager.getRunQemuWithXterm(context) ? "stdio" : "vc";
}
return params;
}
public static String removeDisplayParams(String params) {
return params
.replaceAll("(?<=\\s|^)-object\\s+secret,id=vncpass[^\\s]*", "")
.replaceAll("(?<=\\s|^)-vnc\\b.*?(?=\\s+-monitor\\s+\\S+|$)", "")
.replaceAll("(?<=\\s|^)-vnc\\b[^\\s]*", "")
.replaceAll("(?<=\\s|^)-monitor\\s+\\S+", "")
.replaceAll("(?<=\\s|^)-display\\b.*?(?=\\s+-monitor\\s+\\S+|$)", "")
.replaceAll("(?<=\\s|^)-display\\s+\\S+", "")
.replaceAll("(?<=\\s|^)-spice\\s+\\S+", "")
.replaceAll("\\s{2,}", " ")
.trim();
}
}

View file

@ -918,9 +918,15 @@ public class VMManager {
progressDialog.show();
new Thread(() -> {
pauseCurrentVM();
if (!startMigrate().contains("terminal does not allow synchronous migration, continuing detached")) {
String migrateResult = startMigrate();
if (migrateResult == null || !migrateResult.contains("terminal does not allow synchronous migration, continuing detached")) {
resumeCurrentVM();
_activity.runOnUiThread(progressDialog::reset);
_activity.runOnUiThread(() -> {
DialogUtils.oopsDialog(_activity, _activity.getString(R.string.vm_state_save_failed_note));
progressDialog.reset();
});
Log.e(TAG, "Pause VM failed.");
return;
}

View file

@ -39,6 +39,7 @@ import com.vectras.vm.utils.DeviceUtils;
import com.vectras.vm.utils.DialogUtils;
import com.vectras.vm.utils.FileUtils;
import com.vectras.vm.utils.ImageUtils;
import com.vectras.vm.utils.JSONUtils;
import com.vectras.vm.utils.PackageUtils;
import com.vectras.vm.utils.UIUtils;
@ -989,6 +990,11 @@ public class VMCreatorActivity extends AppCompatActivity {
}
}
} else {
if (!JSONUtils.isValidFromFile(AppConfig.vmFolder + vmID + "/rom-data.json")) {
DialogUtils.oneDialog(this, getResources().getString(R.string.oops), getResources().getString(R.string.error_CR_CVBI4), getResources().getString(R.string.ok), true, R.drawable.warning_48px, true, null, null);
return;
}
loadConfig(new Gson().fromJson(FileUtils.readFromFile(this, new File(AppConfig.vmFolder + vmID + "/rom-data.json")), DataMainRoms.class));
JSONObject jObj = new JSONObject(FileUtils.readFromFile(this, new File(AppConfig.vmFolder + vmID + "/rom-data.json")));
@ -1028,7 +1034,11 @@ public class VMCreatorActivity extends AppCompatActivity {
}
if (FileUtils.isFileExists(AppConfig.vmFolder + vmID + "/cqcm.json")) {
FileUtils.writeToFile(AppConfig.vmFolder + vmID, "cqcm.json", FileUtils.readAFile(AppConfig.vmFolder + vmID + "/cqcm.json").replace("OhnoIjustrealizeditsmidnightandIstillhavetodothis", AppConfig.vmFolder + current.vmID + "/"));
FileUtils.writeToFile(AppConfig.vmFolder + vmID, "cqcm.json", FileUtils.readAFile(AppConfig.vmFolder + vmID + "/cqcm.json").replace("OhnoIjustrealizeditsmidnightandIstillhavetodothis", AppConfig.vmFolder + vmID + "/"));
}
if (FileUtils.isFileExists(AppConfig.vmFolder + vmID + "/snapshot.sh")) {
FileUtils.writeToFile(AppConfig.vmFolder + vmID, "snapshot.sh", FileUtils.readAFile(AppConfig.vmFolder + vmID + "/snapshot.sh").replace("OhnoIjustrealizeditsmidnightandIstillhavetodothis", AppConfig.vmFolder + vmID + "/"));
}
}

View file

@ -5,6 +5,7 @@ import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
@ -12,6 +13,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.color.MaterialColors;
import com.vectras.vm.R;
import com.vectras.vm.databinding.DialogListSelectorLayoutBinding;
import com.vectras.vm.databinding.SimpleLayoutListViewWithCheckBinding;
@ -96,9 +98,11 @@ public class VMCreatorSelector {
public void onBindViewHolder(ViewHolder holder, final int position) {
View view = holder.itemView;
TextView title = view.findViewById(R.id.textview);
ImageView check = view.findViewById(R.id.iv_check);
title.setText(Objects.requireNonNull(data.get(position).get("name")).toString());
title.setTextColor(MaterialColors.getColor(title, position == currentPosition ? androidx.appcompat.R.attr.colorPrimary : com.google.android.material.R.attr.colorOnSurface));
view.setBackgroundResource(position == currentPosition ? R.drawable.dialog_shape_single_button : R.drawable.dialog_shape_click_effect_button);
view.findViewById(R.id.iv_check).setVisibility(position == currentPosition ? View.VISIBLE : View.INVISIBLE);
check.setVisibility(position == currentPosition ? View.VISIBLE : View.INVISIBLE);
view.findViewById(R.id.main).setOnClickListener(v -> {
if (activity.isFinishing() || activity.isDestroyed()) return;
callback.onSelected(

View file

@ -0,0 +1,33 @@
package com.vectras.vm.fcm;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessaging;
public class FCMManager {
private static final String TAG = "FCMManager";
public static void subscribe() {
FirebaseMessaging.getInstance()
.subscribeToTopic("vectrasvmandroidgithub")
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d(TAG, "Subscribed: vectrasvmandroidgithub");
} else {
Log.e(TAG, "Subscription failed: vectrasvmandroidgithub.", task.getException());
}
});
}
public static void unSubscribe() {
FirebaseMessaging.getInstance()
.unsubscribeFromTopic("vectrasvmandroidgithub")
.addOnCompleteListener(task -> {
if (task.isSuccessful()) {
Log.d(TAG, "Unsubscribed: vectrasvmandroidgithub");
} else {
Log.e(TAG, "Cancellation failed: vectrasvmandroidgithub.", task.getException());
}
});
}
}

View file

@ -0,0 +1,34 @@
package com.vectras.vm.fcm;
import androidx.annotation.NonNull;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import com.vectras.vm.R;
import com.vectras.vm.utils.NotificationUtils;
import java.util.Map;
public class FCMService
extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
Map<String, String> data = message.getData();
NotificationUtils.pushNow(this,
1,
NotificationUtils.generalChannelId,
data.get("title") != null ? data.get("title") : getString(R.string.new_notification),
data.get("message") != null ? data.get("message") : getString(R.string.tap_to_view),
R.drawable.ic_vectras_vm_48,
data.get("image") != null ? data.get("image") : null,
-1,
data.get("url") != null ? data.get("url") : null,
null);
}
@Override
public void onNewToken(@NonNull String token) {
}
}

View file

@ -50,6 +50,7 @@ import com.vectras.vm.WebViewActivity;
import com.vectras.vm.databinding.ActivityMainBinding;
import com.vectras.vm.databinding.ActivityMainContentBinding;
import com.vectras.vm.databinding.UpdateBottomDialogLayoutBinding;
import com.vectras.vm.fcm.FCMManager;
import com.vectras.vm.main.romstore.RomStoreHomeAdpater;
import com.vectras.vm.main.softwarestore.SoftwareStoreFragment;
import com.vectras.vm.main.softwarestore.SoftwareStoreHomeAdapter;
@ -352,6 +353,8 @@ public class MainActivity extends AppCompatActivity implements RomStoreFragment.
updateApp();
NotificationUtils.requestPermission(this);
FCMManager.subscribe();
}
@Override

View file

@ -316,6 +316,8 @@ public class MainStartVM {
activity.runOnUiThread(() -> DisplaySystem.launch(context));
}
FileUtils.writeToFile(AppConfig.vmFolder + vmID, "snapshot.sh", env);
Log.i(TAG, "Virtual machine running.");
if (activity != null) {

View file

@ -1000,11 +1000,29 @@ public class FileUtils {
return;
}
Uri uri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".provider",
folder
);
Uri uri;
try {
uri = FileProvider.getUriForFile(
context,
context.getPackageName() + ".provider",
folder
);
} catch (IllegalArgumentException e) {
DialogUtils.oneDialog(
context,
context.getString(R.string.oops),
context.getString(R.string.this_folder_cannot_be_opened),
context.getString(R.string.ok),
true,
R.drawable.error_96px,
true,
null,
null
);
Log.e(TAG, "openFolder: Folder not found!");
return;
}
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri, "resource/folder");

View file

@ -1,11 +1,14 @@
package com.vectras.vm.utils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import com.vectras.vm.AppConfig;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Objects;
@ -28,11 +31,24 @@ public class JSONUtils {
}
}
public static boolean isValidArray(String content) {
if (content == null || content.trim().isEmpty()) {
return false;
}
try {
return JsonParser.parseString(content).isJsonArray();
} catch (Exception e) {
return false;
}
}
public static boolean isValidVmList() {
try {
ArrayList<HashMap<String, Object>> vmList = new Gson().fromJson(FileUtils.readAFile(AppConfig.romsdatajson), new TypeToken<ArrayList<HashMap<String, Object>>>() {
String jsonRaw = FileUtils.readAFile(AppConfig.romsdatajson);
ArrayList<HashMap<String, Object>> vmList = new Gson().fromJson(jsonRaw, new TypeToken<ArrayList<HashMap<String, Object>>>() {
}.getType());
return true;
return isValidArray(jsonRaw);
} catch (Exception e) {
return false;
}

View file

@ -92,7 +92,7 @@ public class TarUtils {
File canonicalDestDir = tarTestDir.getCanonicalFile();
long MAX_FILE_SIZE = 200L * 1024 * 1024; // 200 MB
long MAX_TOTAL_SIZE = 2000L * 1024 * 1024; // 2000 MB
long MAX_TOTAL_SIZE = 4000L * 1024 * 1024; // 4000 MB
int MAX_FILE_COUNT = 10_000;
long totalSize = 0;

View file

@ -7,6 +7,10 @@ public class TextUtils {
return content.matches("\\d+");
}
public static boolean isUnique(String content, String text) {
return content.contains(text) && (content.indexOf(text) == content.lastIndexOf(text));
}
public static String randomALetter() {
String addAdb;
Random random = new Random();

View file

@ -462,11 +462,10 @@ public class X11Activity extends AppCompatActivity implements View.OnApplyWindow
dialog.dismiss();
});
if (!isFinishing()
&& !isDestroyed()
&& getWindow() != null
&& getWindow().getDecorView().isShown()) {
try {
dialog.show();
} catch (WindowManager.BadTokenException e) {
e.printStackTrace();
}
});

View file

@ -38,7 +38,7 @@
android:gravity="center_vertical|end"
android:padding="16dp">
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3Expressive.Button.OutlinedButton"
style="@style/Widget.Material3Expressive.Button.TonalButton"
android:id="@+id/btn_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -2,6 +2,7 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:gravity="center_vertical"
android:padding="16dp"
android:layout_marginHorizontal="16dp"
@ -19,5 +20,6 @@
android:id="@+id/iv_check"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/check_24px"/>
android:src="@drawable/check_24px"
app:tint="?attr/colorPrimary"/>
</LinearLayout>

View file

@ -197,6 +197,7 @@
<string name="error_CR_CVBI2">Đã xảy ra sự cố. Rom này không chứa đầy đủ thông tin và một số cài đặt mặc định đã được áp dụng. Mã lỗi: CR_CVBI2.</string>
<string name="from">Từ</string>
<string name="error_CR_CVBI3">Đã xảy ra lỗi và không thể tiếp tục. Rom này không chứa đầy đủ thông tin và cũng không chứa các tệp ổ đĩa. Mã lỗi: CR_CVBI3.</string>
<string name="error_CR_CVBI4">Đã xảy ra sự cố. Thông tin của rom này đã bị hỏng và một số cài đặt mặc định đã được áp dụng. Mã lỗi: CR_CVBI4.</string>
<string name="clean_up">Dọn dẹp</string>
<string name="clean_up_content">Chà có chút bừa bộn nhỉ? Các rom, tệp và thư mục không còn được sử dụng sẽ bị xoá.</string>
<string name="go_to_rom_store">Đến cửa hàng rom</string>
@ -511,13 +512,16 @@
<string name="an_error_occurred_while_deleting_the_vm">Đã xảy ra lỗi khi xoá máy ảo.</string>
<string name="the_vm_files_were_retained_because_they_are_still_being_used_elsewhere">Các tệp của máy ảo đã được giữ lại vì chúng vẫn còn được sử dụng ở nơi khác.</string>
<string name="pause">Tạm dừng</string>
<string name="pause_vm_note">Trạng thái của máy ảo sẽ được lưu lại và tắt để tiết kiệm tài nguyên. Nó sẽ tiếp tục ở trạng thái này ở lần khởi động tiếp theo. Lưu ý rằng nếu bạn không bật đặt dung lượng bộ nhớ tuỳ chỉnh trong cài đặt bạn nên đặt dung lượng bộ nhớ ở tham số Qemu riêng cho máy ảo này, không được sửa máy ảo này hoặc các cài đặt Qemu ở trong cài đặt của Vectras VM có thể ảnh hưởng đến máy ảo này cho đến lần khởi động tiếp theo. Bất kỳ thay đổi nào cũng có thể khiến việc tải trạng thái máy ảo thất bại.</string>
<string name="pause_vm_note">Trạng thái của máy ảo sẽ được lưu lại và tắt để tiết kiệm tài nguyên. Nó sẽ tiếp tục ở trạng thái này ở lần khởi động tiếp theo. Vui lòng không thay đổi, di chuyển hoặc xoá các tệp được dùng với máy ảo này cho đến lần khởi động tiếp theo.</string>
<string name="pausing_vm_note">Đang tạm dừng máy ảo…\nĐừng rời khỏi đây.</string>
<string name="resuming">Đang tiếp tục…</string>
<string name="vm_resume_error_note">Đã xảy ra sự cố khi tải trạng thái máy ảo đã tạm dừng trước đó để tiếp tục dùng. Nếu bạn đã chỉnh sửa máy ảo này hoặc các cài đặt Qemu trong cài đặt của Vectras VM trước khi khởi động, hãy kiểm tra sửa chúng giống như lúc lần cuối cùng máy ảo này được bật trước đó. Bạn muốn giữ hay xoá bản lưu trạng thái?</string>
<string name="keep_and_continue">Giữ và tiếp tục</string>
<string name="remove_and_continue">Xoá và tiếp tục</string>
<string name="vm_state_save_failed_note">Đã xảy ra lỗi và không thể lưu lại trạng thái của máy ảo. Hãy thử lại sau.</string>
<string name="this_folder_cannot_be_opened">Không thể mở thư mục này.</string>
<string name="new_notification">Thông báo mới</string>
<string name="tap_to_view">Chạm để xem.</string>
<!--======================TERMUX STRINGS====================-->

View file

@ -206,6 +206,7 @@
<string name="error_CR_CVBI2">An error has occurred. Extraction was successful but the cvbi file does not contain information about this ROM so some default settings will be used. Error code: CR_CVBI2.</string>
<string name="from">From</string>
<string name="error_CR_CVBI3">An error has occurred and cannot continue. The extraction was successful but the cvbi file does not contain information for this ROM and does not contain any ROM files. Error code: CR_CVBI3.</string>
<string name="error_CR_CVBI4">An error has occurred. The ROM information is corrupted and some default settings have been applied. Error code: CR_CVBI4.</string>
<string name="clean_up">Clean up</string>
<string name="clean_up_content">ROMs, files and folders that are no longer in use will be deleted.</string>
<string name="go_to_rom_store">Go to ROM store</string>
@ -520,13 +521,16 @@
<string name="an_error_occurred_while_deleting_the_vm">An error occurred while deleting the virtual machine.</string>
<string name="the_vm_files_were_retained_because_they_are_still_being_used_elsewhere">The virtual machine\'s files were retained because they are still being used elsewhere.</string>
<string name="pause">Pause</string>
<string name="pause_vm_note">The virtual machine\'s state will be saved and shut down to conserve resources. It will remain in this state on the next boot. Note that if you haven\'t enabled custom memory allocation in your settings, you should set the memory allocation in the Qemu parameter specifically for this virtual machine. Do not modify this virtual machine or its Qemu settings within the Vectras VM settings, as this may affect the virtual machine until the next boot. Any changes may cause the virtual machine state to fail to load.</string>
<string name="pause_vm_note">The virtual machine\'s state will be saved and shut down to conserve resources. It will remain in this state on the next boot. Please do not change, move, or delete files used with this virtual machine.</string>
<string name="pausing_vm_note">VM is pausing…\nDon\'t leave here.</string>
<string name="resuming">Resuming…</string>
<string name="vm_resume_error_note">An issue occurred while resuming loading of previously paused virtual machine state. If you modified this virtual machine or Qemu settings in the Vectras VM setup before booting, please check that you changed them to the same settings as the last time this virtual machine was powered on. Do you want to keep or remove the saved state?</string>
<string name="keep_and_continue">Keep and continue</string>
<string name="remove_and_continue">Remove and continue</string>
<string name="vm_state_save_failed_note">An error occurred and the virtual machine\'s state could not be saved. Please try again later.</string>
<string name="this_folder_cannot_be_opened">This folder cannot be opened.</string>
<string name="new_notification">New notification</string>
<string name="tap_to_view">Tap to view.</string>
<!--======================TERMUX STRINGS====================-->

View file

@ -9,16 +9,16 @@ buildscript {
}
dependencies {
classpath 'com.android.tools.build:gradle:9.1.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath libs.gradle
classpath libs.kotlin.gradle.plugin
// Android Gradle plugin
// Add other classpaths like Google Services and Firebase Crashlytics if needed here.
classpath 'com.google.gms:google-services:4.4.4' // assuming you need it for your project
classpath 'com.google.firebase:firebase-crashlytics-gradle:3.0.6'
classpath libs.google.services // assuming you need it for your project
classpath libs.firebase.crashlytics.gradle
// assuming you need it for project
}
}
task clean(type: Delete) {
tasks.register('clean', Delete) {
delete rootProject.buildDir
}

View file

@ -4,4 +4,5 @@ SDK_VERSION=21
android.useAndroidX=true
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.strictFullModeForKeepRules=false
android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false

76
gradle/libs.versions.toml Normal file
View file

@ -0,0 +1,76 @@
[versions]
activityKtx = "1.13.0"
agp = "9.1.0"
annotation = "1.9.1"
appcompat = "1.7.1"
comGoogleFirebase = "firebase-crashlytics"
commonsCompress = "1.28.0"
commonsIo = "2.21.0"
constraintlayout = "2.2.1"
coreKtx = "1.18.0"
documentfile = "1.1.0"
drawerlayout = "1.2.0"
espressoCore = "3.7.0"
firebaseBom = "34.11.0"
firebaseCrashlyticsGradle = "3.0.6"
firebaseMessaging = "25.0.1"
glide = "5.0.5"
googleFirebase = "firebase-analytics"
googleFirebaseCrashlytics = "3.0.6"
googleGmsGoogleServices = "4.4.4"
googleServices = "4.4.4"
gradle = "9.1.0"
gradleToolchainsFoojayResolverConvention = "1.0.0"
gson = "2.13.2"
guava = "33.5.0-jre"
junit = "4.13.2"
junitVersion = "1.3.0"
kotlinGradlePlugin = "2.3.20"
lottie = "6.7.1"
material = "1.14.0-alpha10"
preferenceKtx = "1.2.1"
retrofit2 = "3.0.0"
swiperefreshlayout = "1.2.0"
viewpager = "1.1.0"
window = "1.5.1"
[libraries]
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "annotation" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" }
androidx-drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "drawerlayout" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-preference-ktx = { group = "androidx.preference", name = "preference-ktx", version.ref = "preferenceKtx" }
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
androidx-viewpager = { group = "androidx.viewpager", name = "viewpager", version.ref = "viewpager" }
androidx-window = { group = "androidx.window", name = "window", version.ref = "window" }
commons-compress = { group = "org.apache.commons", name = "commons-compress", version.ref = "commonsCompress" }
commons-io = { group = "commons-io", name = "commons-io", version.ref = "commonsIo" }
compiler = { group = "com.github.bumptech.glide", name = "compiler", version.ref = "glide" }
converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit2" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebaseBom" }
firebase-crashlytics-gradle = { group = "com.google.firebase", name = "firebase-crashlytics-gradle", version.ref = "firebaseCrashlyticsGradle" }
firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebaseMessaging" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
glide = { group = "com.github.bumptech.glide", name = "glide", version.ref = "glide" }
google-services = { group = "com.google.gms", name = "google-services", version.ref = "googleServices" }
gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "gradle" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
guava = { group = "com.google.guava", name = "guava", version.ref = "guava" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlinGradlePlugin" }
lottie = { group = "com.airbnb.android", name = "lottie", version.ref = "lottie" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit2" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
com-google-firebase = { id = "com.google.firebase", version.ref = "comGoogleFirebase" }
google-firebase = { id = "com.google.firebase", version.ref = "googleFirebase" }
google-firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "googleFirebaseCrashlytics" }
google-gms-google-services = { id = "com.google.gms.google-services", version.ref = "googleGmsGoogleServices" }
foojay-resolver = { id = "org.gradle.toolchains.foojay-resolver-convention", version.ref = "gradleToolchainsFoojayResolverConvention" }

View file

@ -1,7 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionSha256Sum=60ea723356d81263e8002fec0fcf9e2b0eee0c0850c7a3d7ab0a63f2ccc601f3
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.0-bin.zip
distributionSha256Sum=2ab2958f2a1e51120c326cad6f385153bb11ee93b3c216c5fccebfdfbb7ec6cb
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

25
js/push_notification.js Normal file
View file

@ -0,0 +1,25 @@
import admin from "firebase-admin";
import serviceAccount from './*-*-firebase-adminsdk-*-*.json' with { type: 'json' };
admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});
async function sendNotification() {
try {
const res = await admin.messaging().send({
data: {
title: "Vectras VM",
message: "Welcome!",
image: "https://github.com/xoureldeen/Vectras-VM-Android/blob/master/resources/vectrasvm.png",
url : "https://github.com/xoureldeen/Vectras-VM-Android"
},
topic: "vectrasvmandroidgithub"
});
console.log("Sent:", res);
} catch (err) {
console.error("Error:", err);
}
}
sendNotification();

View file

@ -1,5 +1,5 @@
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
id "org.gradle.toolchains.foojay-resolver-convention" version "1.0.0"
}
rootProject.name = "Vectras VM"

View file

@ -31,5 +31,5 @@ android {
}
dependencies {
implementation 'androidx.annotation:annotation:1.9.1'
implementation libs.androidx.annotation
}

View file

@ -3,7 +3,7 @@ apply plugin: 'maven-publish'
android {
namespace "com.termux.terminal"
compileSdkVersion 34
compileSdkVersion 36
buildTypes {
release {
@ -33,14 +33,14 @@ android {
}
tasks.withType(Test) {
tasks.withType(Test).configureEach {
testLogging {
events "started", "passed", "skipped", "failed"
}
}
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation libs.junit
}

View file

@ -3,10 +3,10 @@ apply plugin: 'maven-publish'
android {
namespace "com.termux.view"
compileSdkVersion 34
compileSdkVersion 36
dependencies {
implementation "androidx.annotation:annotation:1.9.1"
implementation libs.androidx.annotation
api project(":terminal-emulator")
}
@ -36,7 +36,7 @@ android {
}
dependencies {
testImplementation 'junit:junit:4.13.2'
testImplementation libs.junit
}
publishing {

View file

@ -1,15 +1,15 @@
{
"versionCode":"84",
"versionName":"3.8.0",
"versionCode":"94",
"versionName":"3.9.0",
"size": "45 MB",
"url": "https://github.com/xoureldeen/Vectras-VM-Android/releases",
"Message": "<h2>3.8.0</h2>\nBugs fixed.",
"Message": "<h2>3.9.0</h2>\nBugs fixed.",
"cancellable": true,
"versionCodeBeta":"93",
"versionNameBeta":"3.8.9",
"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",
"versionCodeBeta":"94",
"versionNameBeta":"3.9.0",
"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",
"sizeBeta": "45 MB",
"urlBeta": "https://github.com/AnBui2004/Vectras-VM-Emu-Android/releases",
"MessageBeta": "<h2>3.8.9</h2>Bugs fixed.",
"MessageBeta": "<h2>3.9.0</h2>Bugs fixed.",
"cancellableBeta": true
}

View file

@ -1152,6 +1152,24 @@
"vecid": "windows10build10114cvbi",
"id": "99668"
},
{
"rom_name": "Windows 10 build 10120",
"rom_icon": "https://vuetiwuvbyxywfukompp.supabase.co/storage/v1/object/public/getmyos/v1/files/2018/09/17/windows-logo_1_926ed76111646acbbe332bc5af0cf2ce.png",
"rom_url": "https://youtu.be/WK5c26GLb_U",
"rom_path": "Windows 10 build 10120.cvbi",
"rom_avail": true,
"rom_size": "Insider Preview",
"rom_arch": "X86_64",
"rom_kernel": "windows",
"rom_extra": "",
"final_rom_file_name": "",
"desc": "This Rom is from Nguyen Bao An Bui. You can get it on An Bui app: https://play.google.com/store/apps/details?id=com.anbui.app\n\nWindows 10 is a major release of Microsoft's Windows NT operating system. The successor to Windows 8.1, it was released to manufacturing on July 15, 2015, and later to retail on July 29, 2015. Windows 10 was made available for download via MSDN and TechNet, as a free upgrade for retail copies of Windows 8 and Windows 8.1 users via the Microsoft Store, and to Windows 7 users via Windows Update. Unlike previous Windows NT releases, Windows 10 receives new builds on an ongoing basis, which are available at no additional cost to users; devices in enterprise environments can alternatively use long-term support milestones that only receive critical updates, such as security patches.",
"file_size": "9 GB",
"creator": "Nguyen Bao An Bui",
"verified": true,
"vecid": "windows10build10120cvbi",
"id": "99650"
},
{
"rom_name": "Windows 10 build 10240",
"rom_icon": "https://vuetiwuvbyxywfukompp.supabase.co/storage/v1/object/public/getmyos/v1/files/2018/09/17/windows-logo_1_926ed76111646acbbe332bc5af0cf2ce.png",