migrate to our backend for updates

Signed-off-by: androidacy-user <opensource@androidacy.com>
pull/89/head
androidacy-user 2 years ago
parent a9122f572a
commit d0e393c9d6

@ -38,7 +38,7 @@ android {
applicationId = "com.fox2code.mmm"
minSdk = 24
targetSdk = 33
versionCode = 75
versionCode = 76
versionName = "2.1.2"
vectorDrawables {
useSupportLibrary = true

@ -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<String, Int>()
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

@ -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<BottomNavigationItemView>(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<MaterialSwitch>(R.id.setup_agree_eula)
val agreeEula = view.findViewById<MaterialCheckBox>(R.id.setup_agree_eula)
agreeEula.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setupButton.isEnabled = isChecked
}

@ -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<MaterialTextView>(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<MaterialTextView>(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<MaterialTextView>(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<MaterialTextView>(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<MaterialTextView>(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)

@ -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");

@ -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" />
</LinearLayout>

Loading…
Cancel
Save