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" applicationId = "com.fox2code.mmm"
minSdk = 24 minSdk = 24
targetSdk = 33 targetSdk = 33
versionCode = 75 versionCode = 76
versionName = "2.1.2" versionName = "2.1.2"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true

@ -1,5 +1,6 @@
package com.fox2code.mmm 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.Files.Companion.write
import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet
import org.json.JSONObject import org.json.JSONObject
@ -8,20 +9,20 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.charset.StandardCharsets
// See https://docs.github.com/en/rest/reference/repos#releases // See https://docs.github.com/en/rest/reference/repos#releases
@Suppress("unused") @Suppress("unused")
class AppUpdateManager private constructor() { class AppUpdateManager private constructor() {
var changes: String? = null
private val compatDataId = HashMap<String, Int>() private val compatDataId = HashMap<String, Int>()
private val updateLock = Any() private val updateLock = Any()
private val compatFile: File = File(MainApplication.INSTANCE!!.filesDir, "compat.txt") private val compatFile: File = File(MainApplication.INSTANCE!!.filesDir, "compat.txt")
private var latestRelease: String? private var latestRelease: Int?
private var lastChecked: Long private var lastChecked: Long
init { init {
latestRelease = MainApplication.bootSharedPreferences latestRelease = MainApplication.bootSharedPreferences
?.getString("updater_latest_release", BuildConfig.VERSION_NAME) ?.getInt("latest_vcode", BuildConfig.VERSION_CODE)
lastChecked = 0 lastChecked = 0
if (compatFile.isFile) { if (compatFile.isFile) {
try { try {
@ -40,27 +41,29 @@ class AppUpdateManager private constructor() {
lastChecked < System.currentTimeMillis() - 60000L lastChecked < System.currentTimeMillis() - 60000L
) return force && peekShouldUpdate() ) return force && peekShouldUpdate()
synchronized(updateLock) { synchronized(updateLock) {
Timber.d("Checking for app updates")
if (lastChecked != this.lastChecked) return peekShouldUpdate() if (lastChecked != this.lastChecked) return peekShouldUpdate()
try { // make a request to https://production-api.androidacy.com/ammm/updates/check with appVersionCode and token/device_id/client_id
val release = var token = AndroidacyRepoData.token
JSONObject(String(doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8)) if (!AndroidacyRepoData.getInstance().isValidToken(token)) {
var latestRelease: String? = null Timber.w("Invalid token, not checking for updates")
var preRelease = false token = AndroidacyRepoData.getInstance().requestNewToken()
// get latest_release from tag_name translated to int }
if (release.has("tag_name")) { val deviceId = AndroidacyRepoData.generateDeviceId()
latestRelease = release.getString("tag_name") val clientId = BuildConfig.ANDROIDACY_CLIENT_ID
preRelease = release.getBoolean("prerelease") 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)
Timber.d("Latest release: %s, isPreRelease: %s", latestRelease, preRelease) // convert response to string
if (latestRelease == null) return false val responseString = String(response, Charsets.UTF_8)
if (preRelease) { Timber.d("Response: $responseString")
this.latestRelease = "99999999" // prevent updating to pre-release // json response has a boolean shouldUpdate and an int latestVersion
return false JSONObject(responseString).let {
if (it.getBoolean("shouldUpdate")) {
latestRelease = it.getInt("latestVersion")
MainApplication.bootSharedPreferences?.edit()
?.putInt("latest_vcode", latestRelease!!)?.apply()
} }
this.latestRelease = latestRelease this.changes = it.getString("changelog")
this.lastChecked = System.currentTimeMillis()
} catch (ioe: Exception) {
Timber.e(ioe)
} }
} }
return peekShouldUpdate() return peekShouldUpdate()
@ -83,8 +86,8 @@ class AppUpdateManager private constructor() {
var currentVersion = 0 var currentVersion = 0
var latestVersion = 0 var latestVersion = 0
try { try {
currentVersion = BuildConfig.VERSION_NAME.replace("\\D".toRegex(), "").toInt() currentVersion = BuildConfig.VERSION_CODE
latestVersion = latestRelease!!.replace("v", "").replace("\\D".toRegex(), "").toInt() latestVersion = latestRelease!!
} catch (ignored: NumberFormatException) { } catch (ignored: NumberFormatException) {
} }
return currentVersion < latestVersion return currentVersion < latestVersion

@ -23,6 +23,7 @@ import com.fox2code.rosettax.LanguageActivity
import com.fox2code.rosettax.LanguageSwitcher import com.fox2code.rosettax.LanguageSwitcher
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.button.MaterialButton 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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.materialswitch.MaterialSwitch
import com.topjohnwu.superuser.internal.UiThreadHandler import com.topjohnwu.superuser.internal.UiThreadHandler
@ -173,7 +174,7 @@ class SetupActivity : FoxActivity(), LanguageActivity {
// Setup button // Setup button
val setupButton = view.findViewById<BottomNavigationItemView>(R.id.setup_finish) 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 // 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 -> agreeEula.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
setupButton.isEnabled = isChecked setupButton.isEnabled = isChecked
} }

@ -7,6 +7,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import com.fox2code.foxcompat.app.FoxActivity import com.fox2code.foxcompat.app.FoxActivity
import com.fox2code.mmm.androidacy.AndroidacyRepoData
import com.fox2code.mmm.utils.io.net.Http import com.fox2code.mmm.utils.io.net.Http
import com.google.android.material.bottomnavigation.BottomNavigationItemView import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
@ -177,11 +178,10 @@ class UpdateActivity : FoxActivity() {
progressIndicator.isIndeterminate = true progressIndicator.isIndeterminate = true
} }
// check for update // 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 is true, then we have an update
if (shouldUpdate) { if (shouldUpdate) {
runOnUiThread { runOnUiThread {
// set status text to update available // set status text to update available
statusTextView.setText(R.string.update_available) statusTextView.setText(R.string.update_available)
// set button text to download // set button text to download
@ -190,15 +190,25 @@ class UpdateActivity : FoxActivity() {
button.tooltipText = getString(R.string.download_update) button.tooltipText = getString(R.string.download_update)
} }
button.isEnabled = true 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 // return
} else { } else {
runOnUiThread { runOnUiThread {
// set status text to no update available // set status text to no update available
statusTextView.setText(R.string.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 { runOnUiThread {
progressIndicator.isIndeterminate = false progressIndicator.isIndeterminate = false
@ -213,9 +223,20 @@ class UpdateActivity : FoxActivity() {
runOnUiThread { progressIndicator.isIndeterminate = true } runOnUiThread { progressIndicator.isIndeterminate = true }
// get status text view // get status text view
val statusTextView = findViewById<MaterialTextView>(R.id.update_progress_text) 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 { try {
lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false) lastestJSON = Http.doHttpGet(url, false)
} catch (e: Exception) { } catch (e: Exception) {
// when logging, REMOVE the json from the log // when logging, REMOVE the json from the log
Timber.e(e, "Error downloading update info") Timber.e(e, "Error downloading update info")
@ -224,46 +245,34 @@ class UpdateActivity : FoxActivity() {
progressIndicator.setProgressCompat(100, false) progressIndicator.setProgressCompat(100, false)
statusTextView.setText(R.string.error_download_update) statusTextView.setText(R.string.error_download_update)
} }
return
} }
// convert to JSON // convert to JSON
val latestJSON = JSONObject(String(lastestJSON!!)) val latestJSON = JSONObject(String(lastestJSON))
val changelog = latestJSON.getString("body") val changelog = latestJSON.getString("changelog")
// set changelog text. changelog could be markdown, so we need to convert it to HTML runOnUiThread {
val changelogTextView = findViewById<MaterialTextView>(R.id.update_changelog) // set changelog text. changelog could be markdown, so we need to convert it
val markwon = Markwon.builder(this).build() val changelogTextView = findViewById<MaterialTextView>(R.id.update_changelog)
runOnUiThread { markwon.setMarkdown(changelogTextView, changelog) } val markwon = Markwon.create(this@UpdateActivity)
// 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 markwon.setMarkdown(changelogTextView, changelog)
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
} }
// get the download url // get the download url
val downloadUrl = asset?.getString("browser_download_url") var downloadUrl = url.replace("check", "download")
// get the download size // append arch to download url. coerce anything like arm64-* or aarch64-* to arm64 and anything like arm-* or armeabi-* to arm
val downloadSize = asset?.getLong("size") 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 { runOnUiThread {
// set status text to downloading update // set status text to downloading update
statusTextView.text = getString(R.string.downloading_update, 0) statusTextView.text = getString(R.string.downloading_update, 0)
// set progress bar to 0 // set progress bar to 0
@ -273,9 +282,8 @@ class UpdateActivity : FoxActivity() {
// download the update // download the update
var update = ByteArray(0) var update = ByteArray(0)
try { try {
update = Http.doHttpGet(downloadUrl!!) { downloaded: Int, total: Int, _: Boolean -> update = Http.doHttpGet(downloadUrl) { downloaded: Int, total: Int, _: Boolean ->
runOnUiThread { runOnUiThread {
// update progress bar // update progress bar
progressIndicator.setProgressCompat( progressIndicator.setProgressCompat(
(downloaded.toFloat() / total.toFloat() * 100).toInt(), (downloaded.toFloat() / total.toFloat() * 100).toInt(),
@ -308,19 +316,6 @@ class UpdateActivity : FoxActivity() {
// return // return
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 // set status text to installing update
runOnUiThread { runOnUiThread {
statusTextView.setText(R.string.installing_update) statusTextView.setText(R.string.installing_update)

@ -44,7 +44,7 @@ import timber.log.Timber;
@SuppressWarnings("KotlinInternalInJava") @SuppressWarnings("KotlinInternalInJava")
public final class AndroidacyRepoData extends RepoData { public final class AndroidacyRepoData extends RepoData {
public static String ANDROIDACY_DEVICE_ID = null; 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 { static {
HttpUrl.Builder OK_HTTP_URL_BUILDER = new HttpUrl.Builder().scheme("https"); 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 // Try to get the device ID from the shared preferences
SharedPreferences sharedPreferences = MainApplication.getSharedPreferences("androidacy"); 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) { if (deviceIdPref != null) {
ANDROIDACY_DEVICE_ID = deviceIdPref; ANDROIDACY_DEVICE_ID = deviceIdPref;
return deviceIdPref; return deviceIdPref;
} else { } else {
Fingerprinter fp = FingerprinterFactory.create(MainApplication.getINSTANCE().getApplicationContext()); Fingerprinter fp = FingerprinterFactory.create(Objects.requireNonNull(MainApplication.getINSTANCE()).getApplicationContext());
fp.getFingerprint(Fingerprinter.Version.V_5, fingerprint -> { fp.getFingerprint(Fingerprinter.Version.V_5, fingerprint -> {
ANDROIDACY_DEVICE_ID = fingerprint; ANDROIDACY_DEVICE_ID = fingerprint;
// use fingerprint // use fingerprint
@ -142,7 +142,7 @@ public final class AndroidacyRepoData extends RepoData {
if (e.getErrorCode() == 401) { if (e.getErrorCode() == 401) {
Timber.w("Invalid token, resetting..."); Timber.w("Invalid token, resetting...");
// Remove saved preference // 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.remove("pref_androidacy_api_token");
editor.apply(); editor.apply();
return false; return false;
@ -153,19 +153,36 @@ public final class AndroidacyRepoData extends RepoData {
Timber.w("Invalid token, resetting..."); Timber.w("Invalid token, resetting...");
Timber.w(e); Timber.w(e);
// Remove saved preference // 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.remove("pref_androidacy_api_token");
editor.apply(); editor.apply();
return false; 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"}) @SuppressLint({"RestrictedApi", "BinaryOperationInTimber"})
@Override @Override
protected boolean prepare() { protected boolean prepare() {
// If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return // If ANDROIDACY_CLIENT_ID is not set or is empty, disable this repo and return
if (Objects.equals(BuildConfig.ANDROIDACY_CLIENT_ID, "")) { 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.putBoolean("pref_androidacy_repo_enabled", false);
editor.apply(); editor.apply();
Timber.w("ANDROIDACY_CLIENT_ID is empty, disabling AndroidacyRepoData 2"); 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 it's a 400, the app is probably outdated. Show a snackbar suggesting user update app and webview
if (connection.getResponseCode() == 400) { if (connection.getResponseCode() == 400) {
// Show a dialog using androidacy_update_needed string // 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 // Open the app's page on the Play Store
Intent intent = new Intent(Intent.ACTION_VIEW); 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")); 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"); Timber.e(e, "Failed to ping server");
return false; return false;
} }
String deviceId = generateDeviceId();
long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
if (this.androidacyBlockade > time) return true; // fake it till you make it. Basically, 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. // don't fail just because we're rate limited. API and web rate limits are different.
this.androidacyBlockade = time + 30_000L; this.androidacyBlockade = time + 30_000L;
try { try {
if (token == null) { 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)) { if (token != null && !this.isValidToken(token)) {
Timber.i("Token expired or invalid, requesting new one..."); Timber.i("Token expired or invalid, requesting new one...");
token = null; token = null;
@ -229,7 +245,7 @@ public final class AndroidacyRepoData extends RepoData {
try { try {
Timber.i("Requesting new token..."); Timber.i("Requesting new token...");
// POST json request to https://production-api.androidacy.com/auth/register // 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 // Parse token
try { try {
JSONObject jsonObject = new JSONObject(token); JSONObject jsonObject = new JSONObject(token);
@ -257,7 +273,7 @@ public final class AndroidacyRepoData extends RepoData {
return false; return false;
} else { } else {
// Save token to shared preference // 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.putString("pref_androidacy_api_token", token);
editor.apply(); editor.apply();
Timber.i("Token saved to shared preference"); Timber.i("Token saved to shared preference");

@ -293,7 +293,7 @@
android:layout_marginHorizontal="2dp" android:layout_marginHorizontal="2dp"
android:layout_marginVertical="4dp" android:layout_marginVertical="4dp"
android:text="@string/eula_agree_v2" android:text="@string/eula_agree_v2"
android:textAppearance="@style/TextAppearance.Material3.LabelSmall" /> android:textAppearance="@style/TextAppearance.Material3.BodySmall" />
</LinearLayout> </LinearLayout>

Loading…
Cancel
Save