work more on in-app updates

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/284/head
androidacy-user 1 year ago
parent 376ab671ce
commit d763b4b85b

@ -40,10 +40,13 @@
tools:targetApi="tiramisu">
<activity
android:name=".UpdateActivity"
android:hardwareAccelerated="true"
android:process=":updater"
android:exported="false" />
<activity
android:name=".CrashHandler"
android:exported="false"
android:hardwareAccelerated="true"
android:process=":crash" />
<activity
android:name=".SetupActivity"

@ -30,21 +30,17 @@ public class AppUpdateManager {
public static final int FLAG_COMPAT_MMT_REBORN = 0x0100;
public static final int FLAG_COMPAT_ZIP_WRAPPER = 0x0200;
private static final AppUpdateManager INSTANCE = new AppUpdateManager();
private static final String RELEASES_API_URL = "https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases";
public static final String RELEASES_API_URL = "https://api.github.com/repos/Fox2Code/FoxMagiskModuleManager/releases/latest";
private final HashMap<String, Integer> compatDataId = new HashMap<>();
private final Object updateLock = new Object();
private final File compatFile;
private String latestRelease;
private String latestPreRelease;
private long lastChecked;
private boolean preReleaseNewer;
private AppUpdateManager() {
this.compatFile = new File(MainApplication.getINSTANCE().getFilesDir(), "compat.txt");
this.latestRelease = MainApplication.getBootSharedPreferences().getString("updater_latest_release", BuildConfig.VERSION_NAME);
this.latestPreRelease = MainApplication.getBootSharedPreferences().getString("updater_latest_pre_release", BuildConfig.VERSION_NAME);
this.lastChecked = 0;
this.preReleaseNewer = true;
if (this.compatFile.isFile()) {
try {
this.parseCompatibilityFlags(new FileInputStream(this.compatFile));
@ -110,13 +106,6 @@ public class AppUpdateManager {
}
if (latestRelease != null)
this.latestRelease = latestRelease;
if (latestPreRelease != null) {
this.latestPreRelease = latestPreRelease;
this.preReleaseNewer = preReleaseNewer;
} else if (!preReleaseNewer) {
this.latestPreRelease = "";
this.preReleaseNewer = false;
}
if (BuildConfig.DEBUG)
Timber.d("Latest release: %s", latestRelease);
if (BuildConfig.DEBUG)
@ -145,7 +134,7 @@ public class AppUpdateManager {
}
public boolean peekShouldUpdate() {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG)
return false;
// Convert both BuildConfig.VERSION_NAME and latestRelease to int
int currentVersion = 0, latestVersion = 0;
@ -155,20 +144,13 @@ public class AppUpdateManager {
} catch (
NumberFormatException ignored) {
}
int latestPreReleaseVersion = 0;
// replace all non-numeric characters with empty string
try {
latestPreReleaseVersion = Integer.parseInt(this.latestPreRelease.replaceAll("\\D", ""));
} catch (
NumberFormatException ignored) {
}
return currentVersion < latestVersion || (this.preReleaseNewer && currentVersion < latestPreReleaseVersion);
return currentVersion < latestVersion;
}
public boolean peekHasUpdate() {
if (!BuildConfig.ENABLE_AUTO_UPDATER)
if (!BuildConfig.ENABLE_AUTO_UPDATER || BuildConfig.DEBUG)
return false;
return !BuildConfig.VERSION_NAME.equals(this.preReleaseNewer ? this.latestPreRelease : this.latestRelease);
return this.peekShouldUpdate();
}
private void parseCompatibilityFlags(InputStream inputStream) throws IOException {

@ -1,14 +1,286 @@
package com.fox2code.mmm;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import com.fox2code.mmm.utils.io.Http;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.LinearProgressIndicator;
import com.google.android.material.textview.MaterialTextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
@SuppressWarnings("UnnecessaryReturnStatement")
public class UpdateActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_update);
// Get the progress bar and make it indeterminate for now
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
// set status text to please wait
statusTextView.setText(R.string.please_wait);
// for debug builds, set update_debug_warning to visible and return
if (BuildConfig.DEBUG) {
findViewById(R.id.update_debug_warning).setVisibility(MaterialTextView.VISIBLE);
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
statusTextView.setVisibility(MaterialTextView.INVISIBLE);
return;
}
// Now, parse the intent
Bundle extras = getIntent().getExtras();
// if extras is null, then we are in a bad state or user launched the activity manually
if (extras == null) {
// set status text to error
statusTextView.setText(R.string.error_no_extras);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// return
return;
}
// get action
ACTIONS action = ACTIONS.valueOf(extras.getString("action"));
// if action is null, then we are in a bad state or user launched the activity manually
if (Objects.isNull(action)) {
// set status text to error
statusTextView.setText(R.string.error_no_action);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// return
return;
}
// For check action, we need to check if there is an update using the AppUpdateManager.peekShouldUpdate()
if (action == ACTIONS.CHECK) {
checkForUpdate();
} else if (action == ACTIONS.DOWNLOAD) {
try {
downloadUpdate();
} catch (JSONException e) {
e.printStackTrace();
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
}
} else if (action == ACTIONS.INSTALL) {
// ensure path was passed and points to a file within our cache directory
String path = extras.getString("path");
if (path == null) {
// set status text to error
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// return
return;
}
File file = new File(path);
if (!file.exists()) {
// set status text to error
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// return
return;
}
if (!Objects.equals(file.getParentFile(), getCacheDir())) {
// set status text to error
statusTextView.setText(R.string.no_file_found);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// return
return;
}
// set status text to installing
statusTextView.setText(R.string.installing_update);
// set progress bar to indeterminate
progressIndicator.setIndeterminate(true);
// install update
installUpdate(file);
}
}
public void checkForUpdate() {
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
// set status text to checking for update
statusTextView.setText(R.string.checking_for_update);
// set progress bar to indeterminate
progressIndicator.setIndeterminate(true);
// check for update
boolean shouldUpdate = AppUpdateManager.getAppUpdateManager().peekShouldUpdate();
// if shouldUpdate is true, then we have an update
if (shouldUpdate) {
// set status text to update available
statusTextView.setText(R.string.update_available);
// set button text to download
MaterialButton button = findViewById(R.id.update_button);
button.setText(R.string.download_update);
// return
} else {
// set status text to no update available
statusTextView.setText(R.string.no_update_available);
// set progress bar to error
// return
}
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
return;
}
public void downloadUpdate() throws JSONException {
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
byte[] lastestJSON = new byte[0];
try {
lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false);
} catch (
Exception e) {
e.printStackTrace();
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
}
// convert to JSON
JSONObject latestJSON = new JSONObject(new String(lastestJSON));
// we already know that there is an update, so we can get the latest version of our architecture. We're going to have to iterate through the assets to find the one we want
JSONArray assets = latestJSON.getJSONArray("assets");
// get the asset we want
JSONObject asset = null;
for (int i = 0; i < assets.length(); i++) {
JSONObject asset1 = assets.getJSONObject(i);
if (asset1.getString("name").contains(Build.SUPPORTED_ABIS[0])) {
asset = asset1;
break;
}
}
// if asset is null, then we are in a bad state
if (Objects.isNull(asset)) {
// set status text to error
statusTextView.setText(R.string.error_no_asset);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
// return
return;
}
// get the download url
String downloadUrl = Objects.requireNonNull(asset).getString("browser_download_url");
// get the download size
long downloadSize = asset.getLong("size");
// set status text to downloading update
statusTextView.setText(getString(R.string.downloading_update, 0));
// set progress bar to 0
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
// download the update
byte[] update = new byte[0];
try {
update = Http.doHttpGet(downloadUrl, (downloaded, total, done) -> {
// update progress bar
progressIndicator.setProgressCompat((int) (((float) downloaded / (float) total) * 100), true);
// update status text
statusTextView.setText(getString(R.string.downloading_update, (int) (((float) downloaded / (float) total) * 100)));
});
} catch (
Exception e) {
e.printStackTrace();
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
}
// if update is null, then we are in a bad state
if (Objects.isNull(update)) {
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
// return
return;
}
// if update is not the same size as the download size, then we are in a bad state
if (update.length != downloadSize) {
// set status text to error
statusTextView.setText(R.string.error_download_update);
// set progress bar to error
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
// return
return;
}
// set status text to installing update
statusTextView.setText(R.string.installing_update);
// set progress bar to 100
progressIndicator.setIndeterminate(true);
progressIndicator.setProgressCompat(100, false);
// save the update to the cache
File updateFile = null;
try {
updateFile = new File(getCacheDir(), "update.apk");
FileOutputStream fileOutputStream = new FileOutputStream(updateFile);
fileOutputStream.write(update);
fileOutputStream.close();
} catch (
IOException e) {
e.printStackTrace();
progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
}
// install the update
installUpdate(updateFile);
// return
return;
}
private void installUpdate(File updateFile) {
// get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
// set status text to installing update
statusTextView.setText(R.string.installing_update);
// set progress bar to 100
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setIndeterminate(true);
progressIndicator.setProgressCompat(100, false);
// install the update
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
// return
return;
}
public enum ACTIONS {
// action can be CHECK, DOWNLOAD, INSTALL
CHECK, DOWNLOAD, INSTALL
}
}

@ -49,6 +49,7 @@ import com.fox2code.mmm.Constants;
import com.fox2code.mmm.MainActivity;
import com.fox2code.mmm.MainApplication;
import com.fox2code.mmm.R;
import com.fox2code.mmm.UpdateActivity;
import com.fox2code.mmm.androidacy.AndroidacyRepoData;
import com.fox2code.mmm.background.BackgroundUpdateChecker;
import com.fox2code.mmm.installer.InstallerInitializer;
@ -491,12 +492,15 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
linkClickable.setVisible(BuildConfig.ENABLE_AUTO_UPDATER && (BuildConfig.DEBUG || AppUpdateManager.getAppUpdateManager().peekHasUpdate()));
linkClickable.setOnPreferenceClickListener(p -> {
devModeStep = 0;
IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code/FoxMagiskModuleManager/releases");
// open UpdateActivity with CHECK action
Intent intent = new Intent(requireContext(), UpdateActivity.class);
intent.setAction(UpdateActivity.ACTIONS.CHECK.name());
startActivity(intent);
return true;
});
linkClickable.setOnPreferenceLongClickListener(p -> {
String toastText = requireContext().getString(R.string.link_copied);
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Fox2Code/FoxMagiskModuleManager/releases"));
clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "https://github.com/Fox2Code/FoxMagiskModuleManager/releases/latest"));
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true;
});

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp"
android:height="24dp" android:autoMirrored="true"
android:tint="?attr/colorControlNormal" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="@android:color/white" android:pathData="M17,1L7,1c-1.1,0 -2,0.9 -2,2v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2L19,3c0,-1.1 -0.9,-2 -2,-2zM17,19L7,19L7,5h10v14zM16,13h-3L13,8h-2v5L8,13l4,4 4,-4z"/>
</vector>

@ -1,69 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".UpdateActivity">
android:orientation="vertical" tools:context=".UpdateActivity">
<!-- Activity used to download and install app updates -->
<TextView
<!-- first, upgrade icon -->
<ImageView
android:id="@+id/updat_icon"
android:layout_width="101dp"
android:layout_height="93dp"
android:layout_gravity="center"
android:layout_margin="8dp"
android:contentDescription="@string/crash_icon"
android:src="@drawable/baseline_system_update_24" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/update_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_gravity="center"
android:layout_marginTop="16dp"
android:text="@string/update_title"
android:textAppearance="?attr/textAppearanceHeadline6"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
android:textAppearance="?attr/textAppearanceHeadline6" />
<TextView
<com.google.android.material.textview.MaterialTextView
android:id="@+id/update_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_gravity="center"
android:text="@string/update_message"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/update_title" />
android:textAppearance="?attr/textAppearanceBody2" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/update_progress"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/update_subtitle" />
android:layout_marginTop="16dp" />
<TextView
<com.google.android.material.textview.MaterialTextView
android:id="@+id/update_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:textAppearance="?attr/textAppearanceBody2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/update_progress" />
android:textAppearance="?attr/textAppearanceBody2" />
<com.google.android.material.button.MaterialButton
android:id="@+id/update_button"
android:layout_width="wrap_content"
android:text="@string/update_button"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:text="@string/update_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/update_progress_text" />
android:visibility="invisible" />
<!-- Invisible warning for debug builds -->
<com.google.android.material.textview.MaterialTextView
android:id="@+id/update_debug_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
android:text="@string/update_debug_warning"
android:textAppearance="?attr/textAppearanceBody2"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

@ -329,6 +329,14 @@
<string name="background_update_check_excludes">Modules to exclude from automatic update checks</string>
<string name="title_activity_update">In-app Updater</string>
<string name="update_title">Update app</string>
<string name="update_message">An update is available for FoxMMM. Please wait while we download and install it.</string>
<string name="update_button">Please wait...</string>
<string name="update_message">An update may be available for FoxMMM. Please wait while we download and install it.</string>
<string name="update_button">Please wait...</string><string name="error_no_extras">ERROR: Invalid data received</string>
<string name="update_debug_warning">You appear to be running a debug build. Debug builds must be updated manually, and do not support in-app updates</string>
<string name="error_no_action">ERROR: Invalid action specified. Refusing to continue.</string><string name="update_available">Update found</string>
<string name="checking_for_update">Checking for updates…</string>
<string name="no_update_available">You\'re up to date!</string>
<string name="download_update">Download update</string>
<string name="error_download_update">An error occurred downloading the update information.</string>
<string name="error_no_asset">ERROR: Failed to parse update information</string>
<string name="downloading_update">Downloading update… %1$d\%</string><string name="installing_update">Installing update…</string><string name="no_file_found">ERROR: Could not find update package.</string><string name="check_for_updates">Check for app updates</string>
</resources>

@ -62,7 +62,7 @@
app:icon="@drawable/ic_baseline_system_update_24"
app:key="pref_update"
app:singleLineTitle="false"
app:title="@string/app_update" />
app:title="@string/check_for_updates" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/pref_category_appearance">

Loading…
Cancel
Save