finalize in-app updates

need to hook this up to update checks still

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

@ -338,7 +338,7 @@ dependencies {
implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'com.jakewharton.timber:timber:5.0.1'
// ksp // ksp
implementation "com.google.devtools.ksp:symbol-processing-api:1.8.0-1.0.8" implementation "com.google.devtools.ksp:symbol-processing-api:1.8.0-1.0.9"
implementation "androidx.security:security-crypto:1.1.0-alpha04" implementation "androidx.security:security-crypto:1.1.0-alpha04"
} }
@ -364,7 +364,6 @@ android {
} }
buildFeatures { buildFeatures {
viewBinding true viewBinding true
compose true
} }
kotlinOptions { kotlinOptions {

@ -20,6 +20,7 @@
<uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow --> <uses-permission-sdk-23 android:name="android.permission.QUERY_ALL_PACKAGES" /> <!-- Supposed to fix bugs with old firmware, only requested on pre Marshmallow -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Post background notifications --> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Post background notifications -->
<uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission-sdk-23 android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@ -41,7 +42,6 @@
<activity <activity
android:name=".UpdateActivity" android:name=".UpdateActivity"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:process=":updater"
android:exported="false" /> android:exported="false" />
<activity <activity
android:name=".CrashHandler" android:name=".CrashHandler"

@ -3,7 +3,6 @@ package com.fox2code.mmm;
import com.fox2code.mmm.utils.io.Files; import com.fox2code.mmm.utils.io.Files;
import com.fox2code.mmm.utils.io.Http; import com.fox2code.mmm.utils.io.Http;
import org.json.JSONArray;
import org.json.JSONObject; import org.json.JSONObject;
import java.io.BufferedReader; import java.io.BufferedReader;
@ -81,10 +80,16 @@ public class AppUpdateManager {
return this.peekShouldUpdate(); return this.peekShouldUpdate();
boolean preReleaseNewer = true; boolean preReleaseNewer = true;
try { try {
JSONArray releases = new JSONArray(new String(Http.doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8)); JSONObject releases = new JSONObject(new String(Http.doHttpGet(RELEASES_API_URL, false), StandardCharsets.UTF_8));
String latestRelease = null, latestPreRelease = null; String latestRelease = null, latestPreRelease = null;
for (int i = 0; i < releases.length(); i++) { for (int i = 0; i < releases.length(); i++) {
JSONObject release = releases.getJSONObject(i); JSONObject release;
try {
release = releases.getJSONObject(String.valueOf(i));
} catch (
Exception e) {
continue;
}
// Skip invalid entries // Skip invalid entries
if (release.getBoolean("draft")) if (release.getBoolean("draft"))
continue; continue;

@ -375,5 +375,18 @@ public class SetupActivity extends FoxActivity implements LanguageActivity {
IOException e) { IOException e) {
Timber.e(e); Timber.e(e);
} }
// we literally only use these to create the http cache folders
File httpCacheDir = MainApplication.getINSTANCE().getDataDirWithPath("cache/WebView/Default/HTTP Cache/Code Cache/js");
File httpCacheDir2 = MainApplication.getINSTANCE().getDataDirWithPath("cache/WebView/Default/HTTP Cache/Code Cache/wasm");
if (!httpCacheDir.exists()) {
if (httpCacheDir.mkdirs()) {
Timber.d("Created http cache dir");
}
}
if (!httpCacheDir2.exists()) {
if (httpCacheDir2.mkdirs()) {
Timber.d("Created http cache dir");
}
}
} }
} }

@ -1,12 +1,15 @@
package com.fox2code.mmm; package com.fox2code.mmm;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider;
import com.fox2code.foxcompat.app.FoxActivity;
import com.fox2code.mmm.utils.io.Http; import com.fox2code.mmm.utils.io.Http;
import com.google.android.material.button.MaterialButton; import com.google.android.material.button.MaterialButton;
import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.progressindicator.LinearProgressIndicator;
@ -21,8 +24,10 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Objects; import java.util.Objects;
import timber.log.Timber;
@SuppressWarnings("UnnecessaryReturnStatement") @SuppressWarnings("UnnecessaryReturnStatement")
public class UpdateActivity extends AppCompatActivity { public class UpdateActivity extends FoxActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -36,91 +41,105 @@ public class UpdateActivity extends AppCompatActivity {
// set status text to please wait // set status text to please wait
statusTextView.setText(R.string.please_wait); statusTextView.setText(R.string.please_wait);
// for debug builds, set update_debug_warning to visible and return // for debug builds, set update_debug_warning to visible and return
if (BuildConfig.DEBUG) { /**if (BuildConfig.DEBUG) {
findViewById(R.id.update_debug_warning).setVisibility(MaterialTextView.VISIBLE); findViewById(R.id.update_debug_warning).setVisibility(MaterialTextView.VISIBLE);
progressIndicator.setIndeterminate(false); progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false); progressIndicator.setProgressCompat(0, false);
statusTextView.setVisibility(MaterialTextView.INVISIBLE); statusTextView.setVisibility(MaterialTextView.INVISIBLE);
return; return;
} }*/
// Now, parse the intent new Thread(() -> {
Bundle extras = getIntent().getExtras(); // Now, parse the intent
// if extras is null, then we are in a bad state or user launched the activity manually String extras = getIntent().getAction();
if (extras == null) { // if extras is null, then we are in a bad state or user launched the activity manually
// set status text to error if (extras == null) {
statusTextView.setText(R.string.error_no_extras); runOnUiThread(() -> {
// set progress bar to error // set status text to error
progressIndicator.setIndeterminate(false); statusTextView.setText(R.string.error_no_extras);
progressIndicator.setProgressCompat(0, false); // set progress bar to error
// return progressIndicator.setIndeterminate(false);
return; progressIndicator.setProgressCompat(0, false);
} });
// 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; return;
} }
File file = new File(path);
if (!file.exists()) { // get action
// set status text to error ACTIONS action = ACTIONS.valueOf(extras);
statusTextView.setText(R.string.no_file_found); // if action is null, then we are in a bad state or user launched the activity manually
// set progress bar to error if (Objects.isNull(action)) {
progressIndicator.setIndeterminate(false); runOnUiThread(() -> {
progressIndicator.setProgressCompat(0, false); // 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
return; return;
} }
if (!Objects.equals(file.getParentFile(), getCacheDir())) {
// set status text to error // For check action, we need to check if there is an update using the AppUpdateManager.peekShouldUpdate()
statusTextView.setText(R.string.no_file_found); if (action == ACTIONS.CHECK) {
// set progress bar to error checkForUpdate();
progressIndicator.setIndeterminate(false); } else if (action == ACTIONS.DOWNLOAD) {
progressIndicator.setProgressCompat(0, false); try {
// return downloadUpdate();
return; } catch (
JSONException e) {
e.printStackTrace();
runOnUiThread(() -> {
// 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 = getIntent().getStringExtra("path");
if (path == null) {
runOnUiThread(() -> {
// 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;
}
File file = new File(path);
if (!file.exists()) {
runOnUiThread(() -> {
// 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
runOnUiThread(() -> {
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);
} }
// set status text to installing
statusTextView.setText(R.string.installing_update); }).start();
// set progress bar to indeterminate
progressIndicator.setIndeterminate(true);
// install update
installUpdate(file);
}
} }
public void checkForUpdate() { public void checkForUpdate() {
@ -163,7 +182,11 @@ public class UpdateActivity extends AppCompatActivity {
lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false); lastestJSON = Http.doHttpGet(AppUpdateManager.RELEASES_API_URL, false);
} catch ( } catch (
Exception e) { Exception e) {
e.printStackTrace(); // when logging, REMOVE the json from the log
String msg = e.getMessage();
// remove everything from the first { to the last }
msg = Objects.requireNonNull(msg).substring(0, msg.indexOf("{")) + msg.substring(msg.lastIndexOf("}") + 1);
Timber.e(msg);
progressIndicator.setIndeterminate(false); progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false); progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update); statusTextView.setText(R.string.error_download_update);
@ -174,11 +197,14 @@ public class UpdateActivity extends AppCompatActivity {
JSONArray assets = latestJSON.getJSONArray("assets"); JSONArray assets = latestJSON.getJSONArray("assets");
// get the asset we want // get the asset we want
JSONObject asset = null; JSONObject asset = null;
for (int i = 0; i < assets.length(); i++) { // iterate through assets until we find the one that contains Build.SUPPORTED_ABIS[0]
JSONObject asset1 = assets.getJSONObject(i); while (Objects.isNull(asset)) {
if (asset1.getString("name").contains(Build.SUPPORTED_ABIS[0])) { for (int i = 0; i < assets.length(); i++) {
asset = asset1; JSONObject asset1 = assets.getJSONObject(i);
break; if (asset1.getString("name").contains(Build.SUPPORTED_ABIS[0])) {
asset = asset1;
break;
}
} }
} }
// if asset is null, then we are in a bad state // if asset is null, then we are in a bad state
@ -195,52 +221,62 @@ public class UpdateActivity extends AppCompatActivity {
String downloadUrl = Objects.requireNonNull(asset).getString("browser_download_url"); String downloadUrl = Objects.requireNonNull(asset).getString("browser_download_url");
// get the download size // get the download size
long downloadSize = asset.getLong("size"); long downloadSize = asset.getLong("size");
// set status text to downloading update runOnUiThread(() -> {
statusTextView.setText(getString(R.string.downloading_update, 0)); // set status text to downloading update
// set progress bar to 0 statusTextView.setText(getString(R.string.downloading_update, 0));
progressIndicator.setIndeterminate(false); // set progress bar to 0
progressIndicator.setProgressCompat(0, false); progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(0, false);
});
// download the update // download the update
byte[] update = new byte[0]; byte[] update = new byte[0];
try { try {
update = Http.doHttpGet(downloadUrl, (downloaded, total, done) -> { update = Http.doHttpGet(downloadUrl, (downloaded, total, done) -> runOnUiThread(() -> {
// update progress bar // update progress bar
progressIndicator.setProgressCompat((int) (((float) downloaded / (float) total) * 100), true); progressIndicator.setProgressCompat((int) (((float) downloaded / (float) total) * 100), true);
// update status text // update status text
statusTextView.setText(getString(R.string.downloading_update, (int) (((float) downloaded / (float) total) * 100))); statusTextView.setText(getString(R.string.downloading_update, (int) (((float) downloaded / (float) total) * 100)));
}); }));
} catch ( } catch (
Exception e) { Exception e) {
e.printStackTrace(); e.printStackTrace();
progressIndicator.setIndeterminate(false); runOnUiThread(() -> {
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(false);
statusTextView.setText(R.string.error_download_update); progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
});
} }
// if update is null, then we are in a bad state // if update is null, then we are in a bad state
if (Objects.isNull(update)) { if (Objects.isNull(update)) {
// set status text to error runOnUiThread(() -> {
statusTextView.setText(R.string.error_download_update); // set status text to error
// set progress bar to error statusTextView.setText(R.string.error_download_update);
progressIndicator.setIndeterminate(false); // set progress bar to error
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
// return // return
return; return;
} }
// if update is not the same size as the download size, then we are in a bad state // if update is not the same size as the download size, then we are in a bad state
if (update.length != downloadSize) { if (update.length != downloadSize) {
// set status text to error runOnUiThread(() -> {
statusTextView.setText(R.string.error_download_update); // set status text to error
// set progress bar to error statusTextView.setText(R.string.error_download_update);
progressIndicator.setIndeterminate(false); // set progress bar to error
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(false);
progressIndicator.setProgressCompat(100, false);
});
// return // return
return; return;
} }
// set status text to installing update // set status text to installing update
statusTextView.setText(R.string.installing_update); runOnUiThread(() -> {
// set progress bar to 100 statusTextView.setText(R.string.installing_update);
progressIndicator.setIndeterminate(true); // set progress bar to 100
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(true);
progressIndicator.setProgressCompat(100, false);
});
// save the update to the cache // save the update to the cache
File updateFile = null; File updateFile = null;
try { try {
@ -251,9 +287,11 @@ public class UpdateActivity extends AppCompatActivity {
} catch ( } catch (
IOException e) { IOException e) {
e.printStackTrace(); e.printStackTrace();
progressIndicator.setIndeterminate(false); runOnUiThread(() -> {
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(false);
statusTextView.setText(R.string.error_download_update); progressIndicator.setProgressCompat(100, false);
statusTextView.setText(R.string.error_download_update);
});
} }
// install the update // install the update
installUpdate(updateFile); installUpdate(updateFile);
@ -261,19 +299,25 @@ public class UpdateActivity extends AppCompatActivity {
return; return;
} }
@SuppressLint("RestrictedApi")
private void installUpdate(File updateFile) { private void installUpdate(File updateFile) {
// get status text view // get status text view
MaterialTextView statusTextView = findViewById(R.id.update_progress_text); runOnUiThread(() -> {
// set status text to installing update MaterialTextView statusTextView = findViewById(R.id.update_progress_text);
statusTextView.setText(R.string.installing_update); // set status text to installing update
// set progress bar to 100 statusTextView.setText(R.string.installing_update);
LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress); // set progress bar to 100
progressIndicator.setIndeterminate(true); LinearProgressIndicator progressIndicator = findViewById(R.id.update_progress);
progressIndicator.setProgressCompat(100, false); progressIndicator.setIndeterminate(true);
// install the update progressIndicator.setProgressCompat(100, false);
Intent intent = new Intent(Intent.ACTION_VIEW); });
intent.setDataAndType(Uri.fromFile(updateFile), "application/vnd.android.package-archive"); // request install permissions
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Intent intent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
Context context = getApplicationContext();
Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".file-provider", updateFile);
intent.setData(uri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent); startActivity(intent);
// return // return
return; return;

@ -53,8 +53,11 @@ public class BackgroundUpdateChecker extends Worker {
if ("twrp-keep".equals(localModuleInfo.id)) if ("twrp-keep".equals(localModuleInfo.id))
continue; continue;
// exclude all modules with id's stored in the pref pref_background_update_check_excludes // exclude all modules with id's stored in the pref pref_background_update_check_excludes
if (MainApplication.getSharedPreferences().getStringSet("pref_background_update_check_excludes", null).contains(localModuleInfo.id)) try {
continue; if (MainApplication.getSharedPreferences().getStringSet("pref_background_update_check_excludes", null).contains(localModuleInfo.id))
continue;
} catch (Exception ignored) {
}
RepoModule repoModule = repoModules.get(localModuleInfo.id); RepoModule repoModule = repoModules.get(localModuleInfo.id);
localModuleInfo.checkModuleUpdate(); localModuleInfo.checkModuleUpdate();
if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode && !PropUtils.isNullString(localModuleInfo.updateVersion)) { if (localModuleInfo.updateVersionCode > localModuleInfo.versionCode && !PropUtils.isNullString(localModuleInfo.updateVersion)) {

@ -504,6 +504,16 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity {
Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show();
return true; return true;
}); });
// for pref_background_update_check_debug_download, do the same as pref_update except with DOWNLOAD action
Preference debugDownload = findPreference("pref_background_update_check_debug_download");
debugDownload.setVisible(MainApplication.isDeveloper() && MainApplication.isBackgroundUpdateCheckEnabled() && !MainApplication.isWrapped());
debugDownload.setOnPreferenceClickListener(p -> {
devModeStep = 0;
Intent intent = new Intent(requireContext(), UpdateActivity.class);
intent.setAction(UpdateActivity.ACTIONS.DOWNLOAD.name());
startActivity(intent);
return true;
});
if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) { if (BuildConfig.DEBUG || BuildConfig.ENABLE_AUTO_UPDATER) {
linkClickable = findPreference("pref_report_bug"); linkClickable = findPreference("pref_report_bug");
linkClickable.setOnPreferenceClickListener(p -> { linkClickable.setOnPreferenceClickListener(p -> {

@ -298,7 +298,7 @@
<string name="error_saving_logs">Could not save logs</string> <string name="error_saving_logs">Could not save logs</string>
<string name="share_logs">Share FoxMMM logs</string> <string name="share_logs">Share FoxMMM logs</string>
<string name="not_official_build">This app is an unofficial FoxMMM build.</string> <string name="not_official_build">This app is an unofficial FoxMMM build.</string>
<string name="crash_text">Uh-oh, we hit a snag!</string>= <string name="crash_text">Uh-oh, we hit a snag!</string>
<string name="feedback_message">Give us more details about what you were doing when this happened. The more, the merrier!</string> <string name="feedback_message">Give us more details about what you were doing when this happened. The more, the merrier!</string>
<string name="feedback_submit">Submit and restart</string> <string name="feedback_submit">Submit and restart</string>
<string name="please_feedback">Please help us out by telling us what you were trying to do when this happened.</string> <string name="please_feedback">Please help us out by telling us what you were trying to do when this happened.</string>
@ -330,7 +330,8 @@
<string name="title_activity_update">In-app Updater</string> <string name="title_activity_update">In-app Updater</string>
<string name="update_title">Update app</string> <string name="update_title">Update app</string>
<string name="update_message">An update may be available for FoxMMM. Please wait while we download and install it.</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_button">Please wait...</string>
<string name="error_no_extras">ERROR: Invalid data received on launch</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="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="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="checking_for_update">Checking for updates…</string>
@ -338,5 +339,6 @@
<string name="download_update">Download update</string> <string name="download_update">Download update</string>
<string name="error_download_update">An error occurred downloading the update information.</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="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> <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><string name="update_debug_download_pref">Test update download mechanism</string>
</resources> </resources>

@ -57,6 +57,12 @@
app:icon="@drawable/baseline_notification_important_24" app:icon="@drawable/baseline_notification_important_24"
app:title="@string/notification_update_debug_pref" /> app:title="@string/notification_update_debug_pref" />
<!-- For debugging: launch update activity with download action -->
<Preference
app:icon="@drawable/ic_baseline_download_24"
app:key="pref_background_update_check_debug_download"
app:singleLineTitle="false"
app:title="@string/update_debug_download_pref" />
<com.fox2code.mmm.settings.LongClickablePreference <com.fox2code.mmm.settings.LongClickablePreference
app:icon="@drawable/ic_baseline_system_update_24" app:icon="@drawable/ic_baseline_system_update_24"

Loading…
Cancel
Save