From d0e393c9d6a7d82af388a9211784dc2d50392006 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Tue, 13 Jun 2023 14:09:20 -0400 Subject: [PATCH] migrate to our backend for updates Signed-off-by: androidacy-user --- app/build.gradle.kts | 2 +- .../java/com/fox2code/mmm/AppUpdateManager.kt | 51 +++++---- .../java/com/fox2code/mmm/SetupActivity.kt | 3 +- .../java/com/fox2code/mmm/UpdateActivity.kt | 107 +++++++++--------- .../mmm/androidacy/AndroidacyRepoData.java | 38 +++++-- app/src/main/res/layout/activity_setup.xml | 2 +- 6 files changed, 109 insertions(+), 94 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 11ef553..17e5c34 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ android { applicationId = "com.fox2code.mmm" minSdk = 24 targetSdk = 33 - versionCode = 75 + versionCode = 76 versionName = "2.1.2" vectorDrawables { useSupportLibrary = true diff --git a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.kt b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.kt index 42917ca..f6bbbee 100644 --- a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.kt +++ b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.kt @@ -1,5 +1,6 @@ package com.fox2code.mmm +import com.fox2code.mmm.androidacy.AndroidacyRepoData import com.fox2code.mmm.utils.io.Files.Companion.write import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet import org.json.JSONObject @@ -8,20 +9,20 @@ import java.io.File import java.io.FileInputStream import java.io.IOException import java.io.InputStream -import java.nio.charset.StandardCharsets // See https://docs.github.com/en/rest/reference/repos#releases @Suppress("unused") class AppUpdateManager private constructor() { + var changes: String? = null private val compatDataId = HashMap() private val updateLock = Any() private val compatFile: File = File(MainApplication.INSTANCE!!.filesDir, "compat.txt") - private var latestRelease: String? + private var latestRelease: Int? private var lastChecked: Long init { latestRelease = MainApplication.bootSharedPreferences - ?.getString("updater_latest_release", BuildConfig.VERSION_NAME) + ?.getInt("latest_vcode", BuildConfig.VERSION_CODE) lastChecked = 0 if (compatFile.isFile) { try { @@ -40,27 +41,29 @@ class AppUpdateManager private constructor() { lastChecked < System.currentTimeMillis() - 60000L ) return force && peekShouldUpdate() synchronized(updateLock) { + Timber.d("Checking for app updates") if (lastChecked != this.lastChecked) return peekShouldUpdate() - try { - val release = - JSONObject(String(doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8)) - var latestRelease: String? = null - var preRelease = false - // get latest_release from tag_name translated to int - if (release.has("tag_name")) { - latestRelease = release.getString("tag_name") - preRelease = release.getBoolean("prerelease") - } - Timber.d("Latest release: %s, isPreRelease: %s", latestRelease, preRelease) - if (latestRelease == null) return false - if (preRelease) { - this.latestRelease = "99999999" // prevent updating to pre-release - return false + // make a request to https://production-api.androidacy.com/ammm/updates/check with appVersionCode and token/device_id/client_id + var token = AndroidacyRepoData.token + if (!AndroidacyRepoData.getInstance().isValidToken(token)) { + Timber.w("Invalid token, not checking for updates") + token = AndroidacyRepoData.getInstance().requestNewToken() + } + val deviceId = AndroidacyRepoData.generateDeviceId() + val clientId = BuildConfig.ANDROIDACY_CLIENT_ID + val url = "https://production-api.androidacy.com/ammm/updates/check?appVersionCode=${BuildConfig.VERSION_CODE}&token=$token&device_id=$deviceId&client_id=$clientId" + val response = doHttpGet(url, false) + // convert response to string + val responseString = String(response, Charsets.UTF_8) + Timber.d("Response: $responseString") + // json response has a boolean shouldUpdate and an int latestVersion + JSONObject(responseString).let { + if (it.getBoolean("shouldUpdate")) { + latestRelease = it.getInt("latestVersion") + MainApplication.bootSharedPreferences?.edit() + ?.putInt("latest_vcode", latestRelease!!)?.apply() } - this.latestRelease = latestRelease - this.lastChecked = System.currentTimeMillis() - } catch (ioe: Exception) { - Timber.e(ioe) + this.changes = it.getString("changelog") } } return peekShouldUpdate() @@ -83,8 +86,8 @@ class AppUpdateManager private constructor() { var currentVersion = 0 var latestVersion = 0 try { - currentVersion = BuildConfig.VERSION_NAME.replace("\\D".toRegex(), "").toInt() - latestVersion = latestRelease!!.replace("v", "").replace("\\D".toRegex(), "").toInt() + currentVersion = BuildConfig.VERSION_CODE + latestVersion = latestRelease!! } catch (ignored: NumberFormatException) { } return currentVersion < latestVersion diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.kt b/app/src/main/java/com/fox2code/mmm/SetupActivity.kt index b185008..90e3e1e 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.kt +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.kt @@ -23,6 +23,7 @@ import com.fox2code.rosettax.LanguageActivity import com.fox2code.rosettax.LanguageSwitcher import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.button.MaterialButton +import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch import com.topjohnwu.superuser.internal.UiThreadHandler @@ -173,7 +174,7 @@ class SetupActivity : FoxActivity(), LanguageActivity { // Setup button val setupButton = view.findViewById(R.id.setup_finish) // on clicking setup_agree_eula, enable the setup button if it's checked, if it's not, disable it - val agreeEula = view.findViewById(R.id.setup_agree_eula) + val agreeEula = view.findViewById(R.id.setup_agree_eula) agreeEula.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setupButton.isEnabled = isChecked } diff --git a/app/src/main/java/com/fox2code/mmm/UpdateActivity.kt b/app/src/main/java/com/fox2code/mmm/UpdateActivity.kt index 22551f5..d19e618 100644 --- a/app/src/main/java/com/fox2code/mmm/UpdateActivity.kt +++ b/app/src/main/java/com/fox2code/mmm/UpdateActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.view.View import androidx.core.content.FileProvider import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.androidacy.AndroidacyRepoData import com.fox2code.mmm.utils.io.net.Http import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationView @@ -177,11 +178,10 @@ class UpdateActivity : FoxActivity() { progressIndicator.isIndeterminate = true } // check for update - val shouldUpdate = AppUpdateManager.appUpdateManager.peekShouldUpdate() + val shouldUpdate = AppUpdateManager.appUpdateManager.checkUpdate(true) // if shouldUpdate is true, then we have an update if (shouldUpdate) { runOnUiThread { - // set status text to update available statusTextView.setText(R.string.update_available) // set button text to download @@ -190,15 +190,25 @@ class UpdateActivity : FoxActivity() { button.tooltipText = getString(R.string.download_update) } button.isEnabled = true + // set changelog text. changelog could be markdown, so we need to convert it + val changelogTextView = findViewById(R.id.update_changelog) + val markwon = Markwon.create(this@UpdateActivity) + markwon.setMarkdown(changelogTextView, + AppUpdateManager.appUpdateManager.changes.toString() + ) } // return } else { runOnUiThread { // set status text to no update available statusTextView.setText(R.string.no_update_available) + // set changelog text. changelog could be markdown, so we need to convert it + val changelogTextView = findViewById(R.id.update_changelog) + val markwon = Markwon.create(this@UpdateActivity) + markwon.setMarkdown(changelogTextView, + AppUpdateManager.appUpdateManager.changes.toString() + ) } - // set progress bar to error - // return } runOnUiThread { progressIndicator.isIndeterminate = false @@ -213,9 +223,20 @@ class UpdateActivity : FoxActivity() { runOnUiThread { progressIndicator.isIndeterminate = true } // get status text view val statusTextView = findViewById(R.id.update_progress_text) - var lastestJSON: ByteArray? = ByteArray(0) + val lastestJSON: ByteArray? + // get changelog from + // make a request to https://production-api.androidacy.com/ammm/updates/check with appVersionCode and token/device_id/client_id + // and the changelog is the json string in changelog + var token = AndroidacyRepoData.token + if (!AndroidacyRepoData.getInstance().isValidToken(token)) { + Timber.w("Invalid token, not checking for updates") + token = AndroidacyRepoData.getInstance().requestNewToken() + } + val deviceId = AndroidacyRepoData.generateDeviceId() + val clientId = BuildConfig.ANDROIDACY_CLIENT_ID + val url = "https://production-api.androidacy.com/ammm/updates/check?appVersionCode=${BuildConfig.VERSION_CODE}&token=$token&device_id=$deviceId&client_id=$clientId" try { - lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false) + lastestJSON = Http.doHttpGet(url, false) } catch (e: Exception) { // when logging, REMOVE the json from the log Timber.e(e, "Error downloading update info") @@ -224,46 +245,34 @@ class UpdateActivity : FoxActivity() { progressIndicator.setProgressCompat(100, false) statusTextView.setText(R.string.error_download_update) } + return } // convert to JSON - val latestJSON = JSONObject(String(lastestJSON!!)) - val changelog = latestJSON.getString("body") - // set changelog text. changelog could be markdown, so we need to convert it to HTML - val changelogTextView = findViewById(R.id.update_changelog) - val markwon = Markwon.builder(this).build() - runOnUiThread { markwon.setMarkdown(changelogTextView, changelog) } - // 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 - val assets = latestJSON.getJSONArray("assets") - // get the asset we want - var asset: JSONObject? = null - // iterate through assets until we find the one that contains Build.SUPPORTED_ABIS[0] - while (Objects.isNull(asset)) { - for (i in 0 until assets.length()) { - val 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 - runOnUiThread { - statusTextView.setText(R.string.error_no_asset) - // set progress bar to error - progressIndicator.isIndeterminate = false - progressIndicator.setProgressCompat(100, false) - } - // return - return + val latestJSON = JSONObject(String(lastestJSON)) + val changelog = latestJSON.getString("changelog") + runOnUiThread { + // set changelog text. changelog could be markdown, so we need to convert it + val changelogTextView = findViewById(R.id.update_changelog) + val markwon = Markwon.create(this@UpdateActivity) + markwon.setMarkdown(changelogTextView, changelog) } // get the download url - val downloadUrl = asset?.getString("browser_download_url") - // get the download size - val downloadSize = asset?.getLong("size") + var downloadUrl = url.replace("check", "download") + // append arch to download url. coerce anything like arm64-* or aarch64-* to arm64 and anything like arm-* or armeabi-* to arm + downloadUrl += if (Build.SUPPORTED_ABIS[0].contains("arm64") || Build.SUPPORTED_ABIS[0].contains("aarch64")) { + "&arch=arm64" + } else if (Build.SUPPORTED_ABIS[0].contains("arm") || Build.SUPPORTED_ABIS[0].contains("armeabi")) { + "&arch=arm" + } else if (Build.SUPPORTED_ABIS[0].contains("x86_64")) { + "&arch=x86_64" + } else if (Build.SUPPORTED_ABIS[0].contains("x86")) { + "&arch=x86" + } else { + // assume universal and hope for the best, because we don't know what to do + Timber.w("Unknown arch ${Build.SUPPORTED_ABIS[0]} when downloading update, assuming universal") + "&arch=universal" + } runOnUiThread { - // set status text to downloading update statusTextView.text = getString(R.string.downloading_update, 0) // set progress bar to 0 @@ -273,9 +282,8 @@ class UpdateActivity : FoxActivity() { // download the update var update = ByteArray(0) try { - update = Http.doHttpGet(downloadUrl!!) { downloaded: Int, total: Int, _: Boolean -> + update = Http.doHttpGet(downloadUrl) { downloaded: Int, total: Int, _: Boolean -> runOnUiThread { - // update progress bar progressIndicator.setProgressCompat( (downloaded.toFloat() / total.toFloat() * 100).toInt(), @@ -308,19 +316,6 @@ class UpdateActivity : FoxActivity() { // return return } - // if update is not the same size as the download size, then we are in a bad state - if (update.size.toLong() != downloadSize) { - runOnUiThread { - - // set status text to error - statusTextView.setText(R.string.error_download_update) - // set progress bar to error - progressIndicator.isIndeterminate = false - progressIndicator.setProgressCompat(100, false) - } - // return - return - } // set status text to installing update runOnUiThread { statusTextView.setText(R.string.installing_update) diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java index 55fe44a..d9d63fc 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -44,7 +44,7 @@ import timber.log.Timber; @SuppressWarnings("KotlinInternalInJava") public final class AndroidacyRepoData extends RepoData { public static String ANDROIDACY_DEVICE_ID = null; - public static String token = MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", null); + public static String token = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).getString("pref_androidacy_api_token", null); static { HttpUrl.Builder OK_HTTP_URL_BUILDER = new HttpUrl.Builder().scheme("https"); @@ -93,12 +93,12 @@ public final class AndroidacyRepoData extends RepoData { } // Try to get the device ID from the shared preferences SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("androidacy"); - String deviceIdPref = sharedPreferences.getString("device_id_v2", null); + String deviceIdPref = Objects.requireNonNull(sharedPreferences).getString("device_id_v2", null); if (deviceIdPref != null) { ANDROIDACY_DEVICE_ID = deviceIdPref; return deviceIdPref; } else { - Fingerprinter fp = FingerprinterFactory.create(MainApplication.getINSTANCE().getApplicationContext()); + Fingerprinter fp = FingerprinterFactory.create(Objects.requireNonNull(MainApplication.getINSTANCE()).getApplicationContext()); fp.getFingerprint(Fingerprinter.Version.V_5, fingerprint -> { ANDROIDACY_DEVICE_ID = fingerprint; // use fingerprint @@ -142,7 +142,7 @@ public final class AndroidacyRepoData extends RepoData { if (e.getErrorCode() == 401) { Timber.w("Invalid token, resetting..."); // Remove saved preference - SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit(); + SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit(); editor.remove("pref_androidacy_api_token"); editor.apply(); return false; @@ -153,19 +153,36 @@ public final class AndroidacyRepoData extends RepoData { Timber.w("Invalid token, resetting..."); Timber.w(e); // Remove saved preference - SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit(); + SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit(); editor.remove("pref_androidacy_api_token"); editor.apply(); return false; } } + /** + * Request a new token from the server and save it to the shared preferences + * @return String token + */ + public String requestNewToken() throws IOException, JSONException { + String deviceId = generateDeviceId(); + byte[] resp = Http.doHttpGet("https://" + this.host + "/auth/register?device_id=" + deviceId + "&client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, false); + // response is JSON + JSONObject jsonObject = new JSONObject(new String(resp)); + String token = jsonObject.getString("token"); + // Save the token to the shared preferences + SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit(); + editor.putString("pref_androidacy_api_token", token); + editor.apply(); + return token; + } + @SuppressLint({"RestrictedApi", "BinaryOperationInTimber"}) @Override protected boolean prepare() { // If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { - SharedPreferences.Editor editor = MainApplication.getSharedPreferences("mmm").edit(); + SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("mmm")).edit(); editor.putBoolean("pref_androidacy_repo_enabled", false); editor.apply(); Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 2"); @@ -183,7 +200,7 @@ public final class AndroidacyRepoData extends RepoData { // If it's a 400, the app is probably outdated. Show a snackbar suggesting user update app and webview if (connection.getResponseCode() == 400) { // Show a dialog using androidacy_update_needed string - new MaterialAlertDialogBuilder(MainApplication.getINSTANCE()).setTitle(R.string.androidacy_update_needed).setMessage(R.string.androidacy_update_needed_message).setPositiveButton(R.string.update, (dialog, which) -> { + new MaterialAlertDialogBuilder(Objects.requireNonNull(MainApplication.getINSTANCE())).setTitle(R.string.androidacy_update_needed).setMessage(R.string.androidacy_update_needed_message).setPositiveButton(R.string.update, (dialog, which) -> { // Open the app's page on the Play Store Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("https://www.androidacy.com/downloads/?view=FoxMMM&utm_source=foxmnm&utm_medium=app&utm_campaign=android-app")); @@ -197,14 +214,13 @@ public final class AndroidacyRepoData extends RepoData { Timber.e(e, "Failed to ping server"); return false; } - String deviceId = generateDeviceId(); long time = System.currentTimeMillis(); if (this.androidacyBlockade > time) return true; // fake it till you make it. Basically, // don't fail just because we're rate limited. API and web rate limits are different. this.androidacyBlockade = time + 30_000L; try { if (token == null) { - token = MainApplication.getSharedPreferences("androidacy").getString("pref_androidacy_api_token", null); + token = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).getString("pref_androidacy_api_token", null); if (token != null && !this.isValidToken(token)) { Timber.i("Token expired or invalid, requesting new one..."); token = null; @@ -229,7 +245,7 @@ public final class AndroidacyRepoData extends RepoData { try { Timber.i("Requesting new token..."); // POST json request to https://production-api.androidacy.com/auth/register - token = new String(Http.doHttpPost("https://" + this.host + "/auth/register?client_id=" + BuildConfig.ANDROIDACY_CLIENT_ID, "{\"device_id\":\"" + deviceId + "\"}", false)); + token = requestNewToken(); // Parse token try { JSONObject jsonObject = new JSONObject(token); @@ -257,7 +273,7 @@ public final class AndroidacyRepoData extends RepoData { return false; } else { // Save token to shared preference - SharedPreferences.Editor editor = MainApplication.getSharedPreferences("androidacy").edit(); + SharedPreferences.Editor editor = Objects.requireNonNull(MainApplication.getSharedPreferences("androidacy")).edit(); editor.putString("pref_androidacy_api_token", token); editor.apply(); Timber.i("Token saved to shared preference"); diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index f2ad473..78d3e62 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -293,7 +293,7 @@ android:layout_marginHorizontal="2dp" android:layout_marginVertical="4dp" android:text="@string/eula_agree_v2" - android:textAppearance="@style/TextAppearance.Material3.LabelSmall" /> + android:textAppearance="@style/TextAppearance.Material3.BodySmall" />