From 3b115044b7673d0fb71880ad2819ced2067b6e12 Mon Sep 17 00:00:00 2001 From: androidacy-user Date: Thu, 20 Apr 2023 21:40:00 -0400 Subject: [PATCH] (fix) misc fixes also ensure ui consistency Signed-off-by: androidacy-user --- .../java/com/fox2code/mmm/MainActivity.java | 19 +----- .../com/fox2code/mmm/MainApplication.java | 49 ++++++++------- .../java/com/fox2code/mmm/SetupActivity.java | 4 +- .../mmm/installer/InstallerActivity.java | 55 +++++++--------- .../fox2code/mmm/manager/ModuleManager.java | 2 +- .../fox2code/mmm/repo/CustomRepoManager.java | 4 +- .../java/com/fox2code/mmm/repo/RepoData.java | 14 +++-- .../com/fox2code/mmm/repo/RepoUpdater.java | 8 +-- .../mmm/settings/SettingsActivity.java | 12 ++-- .../com/fox2code/mmm/utils/TimberUtils.kt | 28 +++++++++ .../com/fox2code/mmm/utils/io/net/Http.java | 63 ++++++------------- .../res/drawable/baseline_arrow_back_24.xml | 5 ++ app/src/main/res/layout/activity_setup.xml | 26 ++++---- app/src/main/res/layout/installer.xml | 16 ++--- app/src/main/res/menu/bottom_nav_install.xml | 21 +++++++ app/src/main/res/values/strings.xml | 1 + 16 files changed, 172 insertions(+), 155 deletions(-) create mode 100644 app/src/main/java/com/fox2code/mmm/utils/TimberUtils.kt create mode 100644 app/src/main/res/drawable/baseline_arrow_back_24.xml create mode 100644 app/src/main/res/menu/bottom_nav_install.xml diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index c3ff15c..b83c849 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -1,6 +1,6 @@ package com.fox2code.mmm; -import static com.fox2code.mmm.MainApplication.isOfficial; +import static com.fox2code.mmm.MainApplication.Iof; import static com.fox2code.mmm.manager.ModuleInfo.FLAG_MM_REMOTE_MODULE; import android.Manifest; @@ -55,10 +55,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.android.material.snackbar.Snackbar; -import org.chromium.net.CronetEngine; import org.matomo.sdk.extra.TrackHelper; -import java.net.URL; import java.util.Objects; import io.realm.Realm; @@ -85,7 +83,6 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe private CardView searchCard; private SearchView searchView; private boolean initMode; - private boolean urlFactoryInstalled = false; public MainActivity() { this.moduleViewListBuilder = new ModuleViewListBuilder(this); @@ -103,16 +100,6 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe @Override protected void onCreate(Bundle savedInstanceState) { this.initMode = true; - // Ensure HTTP Cache directories are created - Http.ensureCacheDirs(this); - if (!urlFactoryInstalled) { - urlFactoryInstalled = true; - try { - URL.setURLStreamHandlerFactory(new CronetEngine.Builder(this).build().createURLStreamHandlerFactory()); - } catch (Error ignored) { - // Ignore - } - } if (doSetupRestarting) { doSetupRestarting = false; } @@ -120,7 +107,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe super.onCreate(savedInstanceState); TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker()); // track enabled repos - RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build(); + RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).build(); Realm realm = Realm.getInstance(realmConfig); StringBuilder enabledRepos = new StringBuilder(); realm.executeTransaction(r -> { @@ -133,7 +120,7 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe } TrackHelper.track().event("enabled_repos", enabledRepos.toString()).with(MainApplication.getINSTANCE().getTracker()); // log all shared preferences that are present - if (!isOfficial) { + if (!Iof) { Timber.w("You may be running an untrusted build."); // Show a toast to warn the user Toast.makeText(this, R.string.not_official_build, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 7b56ff9..ed4d5f4 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -31,6 +31,7 @@ import com.fox2code.foxcompat.app.FoxApplication; import com.fox2code.foxcompat.app.internal.FoxProcessExt; import com.fox2code.foxcompat.view.FoxThemeWrapper; import com.fox2code.mmm.installer.InstallerInitializer; +import com.fox2code.mmm.utils.TimberUtils; import com.fox2code.mmm.utils.io.GMSProviderInstaller; import com.fox2code.mmm.utils.io.net.Http; import com.fox2code.mmm.utils.sentry.SentryMain; @@ -80,9 +81,6 @@ import io.noties.markwon.html.HtmlPlugin; import io.noties.markwon.image.ImagesPlugin; import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler; import io.realm.Realm; -import io.sentry.Sentry; -import io.sentry.SentryLevel; -import io.sentry.android.timber.SentryTimberTree; import timber.log.Timber; @SuppressWarnings("CommentedOutCode") @@ -96,7 +94,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con // Use FoxProcess wrapper helper. private static final boolean wrapped = !FoxProcessExt.isRootLoader(); private static final ArrayList callers = new ArrayList<>(); - public static boolean isOfficial = false; + public static boolean Iof = false; private static String SHOWCASE_MODE_TRUE = null; private static long secret; private static Locale timeFormatLocale = Resources.getSystem().getConfiguration().getLocales().get(0); @@ -125,6 +123,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con private Markwon markwon; private byte[] existingKey; private Tracker tracker; + private boolean makingNewKey = false; public MainApplication() { if (INSTANCE != null && INSTANCE != this) @@ -406,20 +405,13 @@ public class MainApplication extends FoxApplication implements androidx.work.Con relPackageName = this.getPackageName(); super.onCreate(); SentryMain.initialize(this); - // init timber - if (BuildConfig.DEBUG) { - Timber.plant(new Timber.DebugTree()); - } else { - if (isCrashReportingEnabled()) { - //noinspection UnstableApiUsage - Timber.plant(new SentryTimberTree(Sentry.getCurrentHub(), SentryLevel.ERROR, SentryLevel.ERROR)); - } else { - Timber.plant(new ReleaseTree()); - } - } + // dirty workaround so timber doesn't bitch at us + TimberUtils.configTimber(); Timber.i("Starting FoxMMM version %s (%d) - commit %s", BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, BuildConfig.COMMIT_HASH); // Update SSL Ciphers if update is possible GMSProviderInstaller.installIfNeeded(this); + Http.ensureCacheDirs(); + Http.ensureURLHandler(this); Timber.d("Initializing FoxMMM"); Timber.d("Started from background: %s", !isInForeground()); Timber.d("FoxMMM is running in debug mode"); @@ -451,17 +443,16 @@ public class MainApplication extends FoxApplication implements androidx.work.Con } else { Timber.d("Matomo already has install"); } - // Determine if this is an official build based on the signature try { - // Get the signature of the key used to sign the app @SuppressLint("PackageManagerGetSignatures") Signature[] s = this.getPackageManager().getPackageInfo(this.getPackageName(), PackageManager.GET_SIGNATURES).signatures; @SuppressWarnings("SpellCheckingInspection") String[] osh = new String[]{"7bec7c4462f4aac616612d9f56a023ee3046e83afa956463b5fab547fd0a0be6", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}; + //noinspection SpellCheckingInspection String oosh = Hashing.sha256().hashBytes(s[0].toByteArray()).toString(); - isOfficial = Arrays.asList(osh).contains(oosh); + Iof = Arrays.asList(osh).contains(oosh); } catch (PackageManager.NameNotFoundException ignored) { } // hide this behind a buildconfig flag for now, but crash the app if it's not an official build and not debug - if (BuildConfig.ENABLE_PROTECTION && !isOfficial && !BuildConfig.DEBUG) { + if (BuildConfig.ENABLE_PROTECTION && !Iof && !BuildConfig.DEBUG) { throw new RuntimeException("This is not an official build of FoxMMM"); } SharedPreferences sharedPreferences = MainApplication.getPreferences("mmm"); @@ -606,11 +597,26 @@ public class MainApplication extends FoxApplication implements androidx.work.Con } // Create a key to encrypt a realm and save it securely in the keystore - public byte[] getNewKey() { + public byte[] getKey() { + if (makingNewKey) { + // sleep until the key is made + while (makingNewKey) try { + //noinspection BusyWait + Thread.sleep(100); + } catch (InterruptedException ignored) { + // silence is bliss + } + } + // attempt to read the existingKey property + if (existingKey != null) { + return existingKey; + } // check if we have a key already SharedPreferences sharedPreferences = MainApplication.getPreferences("realm_key"); if (sharedPreferences.contains("iv_and_encrypted_key")) { return getExistingKey(); + } else { + makingNewKey = true; } // open a connection to the android keystore KeyStore keyStore; @@ -677,6 +683,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con Timber.d("Created all keys successfully."); MainApplication.getPreferences("realm_key").edit().putString("iv_and_encrypted_key", Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)).apply(); Timber.d("Saved the encrypted key in shared preferences."); + makingNewKey = false; return realmKey; // pass to a realm configuration via encryptionKey() } @@ -747,7 +754,7 @@ public class MainApplication extends FoxApplication implements androidx.work.Con updateModules = new ArrayList<>(); } - private static class ReleaseTree extends Timber.Tree { + public static class ReleaseTree extends Timber.Tree { @Override protected void log(int priority, String tag, @NonNull String message, Throwable t) { // basically silently drop all logs below error, and write the rest to logcat diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.java b/app/src/main/java/com/fox2code/mmm/SetupActivity.java index aaf564f..1459321 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.java +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.java @@ -174,7 +174,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { editor.putBoolean("pref_analytics_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_app_analytics))).isChecked()); Timber.d("Saving preferences"); // Set the repos in the ReposList realm db - RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); boolean androidacyRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).isChecked(); boolean magiskAltRepo = ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).isChecked(); Realm realm = Realm.getInstance(realmConfig); @@ -292,7 +292,7 @@ public class SetupActivity extends FoxActivity implements LanguageActivity { long startTime = System.currentTimeMillis(); // create encryption key Timber.d("Creating encryption key"); - byte[] key = MainApplication.getINSTANCE().getNewKey(); + byte[] key = MainApplication.getINSTANCE().getKey(); // create the realm database for ReposList // create the realm configuration RealmConfiguration config = new RealmConfiguration.Builder().name("ReposList.realm").directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).encryptionKey(key).build(); diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java index f8ff993..3c5c4a3 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -37,8 +37,8 @@ import com.fox2code.mmm.utils.io.PropUtils; import com.fox2code.mmm.utils.io.net.Http; import com.fox2code.mmm.utils.sentry.SentryBreadcrumb; import com.fox2code.mmm.utils.sentry.SentryMain; +import com.google.android.material.bottomnavigation.BottomNavigationItemView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.topjohnwu.superuser.CallbackList; import com.topjohnwu.superuser.Shell; @@ -57,6 +57,7 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.util.Enumeration; import java.util.HashSet; +import java.util.Objects; import java.util.zip.ZipEntry; import timber.log.Timber; @@ -64,7 +65,8 @@ import timber.log.Timber; public class InstallerActivity extends FoxActivity { private static final HashSet extracted = new HashSet<>(); public LinearProgressIndicator progressIndicator; - public ExtendedFloatingActionButton rebootFloatingButton; + public BottomNavigationItemView rebootFloatingButton; + public BottomNavigationItemView cancelFloatingButton; public InstallerTerminal installerTerminal; private File moduleCache; private File toDelete; @@ -102,7 +104,7 @@ public class InstallerActivity extends FoxActivity { return; } // ensure the intent is from our app, and is either a url or within our directory. replace all instances of .. and url encoded .. - target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH).trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", ""); + target = Objects.requireNonNull(intent.getStringExtra(Constants.EXTRA_INSTALL_PATH)).trim().replaceAll("\\.\\.", "").replaceAll("%2e%2e", ""); if (target.isEmpty() || !target.startsWith(MainApplication.getINSTANCE().getDataDir().getAbsolutePath()) && !target.startsWith("https://")) { this.forceBackPressed(); return; @@ -148,6 +150,7 @@ public class InstallerActivity extends FoxActivity { RecyclerView installTerminal; this.progressIndicator = findViewById(R.id.progress_bar); this.rebootFloatingButton = findViewById(R.id.install_terminal_reboot_fab); + this.cancelFloatingButton = findViewById(R.id.back_installer); this.installerTerminal = new InstallerTerminal(installTerminal = findViewById(R.id.install_terminal), this.isLightTheme(), foreground, mmtReborn); (horizontalScroller != null ? horizontalScroller : installTerminal).setBackground(new ColorDrawable(background)); installTerminal.setItemAnimator(null); @@ -528,6 +531,8 @@ public class InstallerActivity extends FoxActivity { } }); this.rebootFloatingButton.setVisibility(View.VISIBLE); + // handle back button + this.cancelFloatingButton.setOnClickListener(_view -> this.finishAndRemoveTask()); if (message != null && !message.isEmpty()) this.installerTerminal.addLine(message); if (optionalLink != null && !optionalLink.isEmpty()) { this.setActionBarExtraMenuButton(ActionButtonType.supportIconForUrl(optionalLink), menu -> { @@ -618,25 +623,13 @@ public class InstallerActivity extends FoxActivity { command = rawCommand; } switch (command) { - case "useRecovery": - this.useRecovery = true; - break; - case "addLine": - this.terminal.addLine(arg); - break; - case "setLastLine": - this.terminal.setLastLine(arg); - break; - case "clearTerminal": - this.terminal.clearTerminal(); - break; - case "scrollUp": - this.terminal.scrollUp(); - break; - case "scrollDown": - this.terminal.scrollDown(); - break; - case "showLoading": + case "useRecovery" -> this.useRecovery = true; + case "addLine" -> this.terminal.addLine(arg); + case "setLastLine" -> this.terminal.setLastLine(arg); + case "clearTerminal" -> this.terminal.clearTerminal(); + case "scrollUp" -> this.terminal.scrollUp(); + case "scrollDown" -> this.terminal.scrollDown(); + case "showLoading" -> { this.isRecoveryBar = false; if (!arg.isEmpty()) { try { @@ -661,26 +654,24 @@ public class InstallerActivity extends FoxActivity { this.progressIndicator.setIndeterminate(true); } this.progressIndicator.setVisibility(View.VISIBLE); - break; - case "setLoading": + } + case "setLoading" -> { this.isRecoveryBar = false; try { this.progressIndicator.setProgressCompat(Short.parseShort(arg), true); } catch (Exception ignored) { } - break; - case "hideLoading": + } + case "hideLoading" -> { this.isRecoveryBar = false; this.progressIndicator.setVisibility(View.GONE); - break; - case "setSupportLink": + } + case "setSupportLink" -> { // Only set link if valid if (arg.isEmpty() || (arg.startsWith("https://") && arg.indexOf('/', 8) > 8)) this.supportLink = arg; - break; - case "disableANSI": - this.terminal.disableAnsi(); - break; + } + case "disableANSI" -> this.terminal.disableAnsi(); } } diff --git a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java index ae97471..1f9071e 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java @@ -97,7 +97,7 @@ public final class ModuleManager extends SyncManager { // if the dir name matches the module name, use it as the cache dir File tempCacheRoot = new File(dir.toString()); Timber.d("Looking for cache in %s", tempCacheRoot); - realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(tempCacheRoot).build(); + realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(tempCacheRoot).build(); Realm realm = Realm.getInstance(realmConfiguration); Timber.d("Looking for cache for %s out of %d", module, realm.where(ModuleListCache.class).count()); moduleListCache = realm.where(ModuleListCache.class).equalTo("codename", module).findFirst(); diff --git a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java index ed2e2d7..246f489 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/CustomRepoManager.java @@ -30,7 +30,7 @@ public class CustomRepoManager { if (MainApplication.getPreferences("mmm").getString("last_shown_setup", "").equals("")) { return; } - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); if (realm.isInTransaction()) { realm.commitTransaction(); @@ -118,7 +118,7 @@ public class CustomRepoManager { } catch (Exception e) { submitModule = null; } - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); int finalI = i; String finalWebsite = website; diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java index fe6f3bd..4978d1e 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -39,6 +39,7 @@ public class RepoData extends XRepo { public final JSONObject supportedProperties = new JSONObject(); private final Object populateLock = new Object(); public String url; + public String id; public File cacheRoot; public SharedPreferences cachedPreferences; @@ -106,7 +107,7 @@ public class RepoData extends XRepo { this.defaultName = url; // Set url as default name this.forceHide = AppUpdateManager.shouldForceHide(this.id); // this.enable is set from the database - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst(); if (reposList == null) { @@ -283,7 +284,7 @@ public class RepoData extends XRepo { @Override public boolean isEnabled() { - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); AtomicBoolean dbEnabled = new AtomicBoolean(false); realm2.executeTransaction(realm -> { @@ -307,7 +308,7 @@ public class RepoData extends XRepo { public void setEnabled(boolean enabled) { this.enabled = enabled && !this.forceHide; // reposlist realm - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); realm2.executeTransaction(realm -> { ReposList reposList = realm.where(ReposList.class).equalTo("id", this.id).findFirst(); @@ -327,9 +328,10 @@ public class RepoData extends XRepo { Timber.e("Repo ID is null"); return; } + // if repo starts with repo_, it's always enabled bc custom repos can't be disabled without being deleted. this.forceHide = AppUpdateManager.shouldForceHide(this.id); // reposlist realm - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); boolean dbEnabled = false; try { @@ -386,12 +388,12 @@ public class RepoData extends XRepo { // should update (lastUpdate > 15 minutes) public boolean shouldUpdate() { Timber.d("Repo " + this.id + " should update check called"); - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); ReposList repo = realm2.where(ReposList.class).equalTo("id", this.id).findFirst(); // Make sure ModuleListCache for repoId is not null File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.id); - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); Realm realm = Realm.getInstance(realmConfiguration); RealmResults moduleListCache = realm.where(ModuleListCache.class).equalTo("repoId", this.id).findAll(); if (repo != null) { diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java index a8c5862..8c34fc3 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -48,11 +48,11 @@ public class RepoUpdater { if (!this.repoData.shouldUpdate()) { Timber.d("Fetching index from cache for %s", this.repoData.id); File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id); - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); Realm realm = Realm.getInstance(realmConfiguration); RealmResults results = realm.where(ModuleListCache.class).equalTo("repoId", this.repoData.id).findAll(); // reposlist realm - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); this.toUpdate = Collections.emptyList(); this.toApply = new HashSet<>(); @@ -121,7 +121,7 @@ public class RepoUpdater { // use realm to insert to // props avail: File cacheRoot = MainApplication.getINSTANCE().getDataDirWithPath("realms/repos/" + this.repoData.id); - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ModuleListCache.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).schemaVersion(1).deleteRealmIfMigrationNeeded().allowWritesOnUiThread(true).allowQueriesOnUiThread(true).directory(cacheRoot).build(); // array with module info default values // supported properties for a module //id= @@ -337,7 +337,7 @@ public class RepoUpdater { Timber.w("Failed to get module info from %s with error %s", this.repoData.id, e.getMessage()); } this.indexRaw = null; - RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration2 = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm2 = Realm.getInstance(realmConfiguration2); if (realm2.isInTransaction()) { realm2.cancelTransaction(); diff --git a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java index 9b45852..64d8da5 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -835,7 +835,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { String flavor = BuildConfig.FLAVOR; String type = BuildConfig.BUILD_TYPE; // Set the summary of pref_pkg_info to something like default-debug v1.0 (123) (Official) - String pkgInfo = getString(R.string.pref_pkg_info_summary, flavor + "-" + type, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, MainApplication.isOfficial ? getString(R.string.official) : getString(R.string.unofficial)); + String pkgInfo = getString(R.string.pref_pkg_info_summary, flavor + "-" + type, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE, MainApplication.Iof ? getString(R.string.official) : getString(R.string.unofficial)); findPreference("pref_pkg_info").setSummary(pkgInfo); // special easter egg :) var ref = new Object() { @@ -957,7 +957,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { }); } // Get magisk_alt_repo enabled state from realm db - RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfig = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm1 = Realm.getInstance(realmConfig); ReposList reposList = realm1.where(ReposList.class).equalTo("id", "magisk_alt_repo").findFirst(); if (reposList != null) { @@ -994,7 +994,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { SwitchPreferenceCompat switchPreferenceCompat = (SwitchPreferenceCompat) androidacyRepoEnabled; switchPreferenceCompat.setChecked(false); // Disable in realm db - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); realm.executeTransaction(realm2 -> { ReposList repoRealmResults = realm2.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst(); @@ -1007,7 +1007,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { }); } // get if androidacy repo is enabled from realm db - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); ReposList repoRealmResults = realm.where(ReposList.class).equalTo("id", "androidacy_repo").findFirst(); if (repoRealmResults == null) { @@ -1164,7 +1164,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { @SuppressLint("RestrictedApi") public void updateCustomRepoList(boolean initial) { - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); // get all repos that are not built-in int CUSTOM_REPO_ENTRIES = 0; @@ -1304,7 +1304,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (preference == null) return; if (!preferenceName.contains("androidacy") && !preferenceName.contains("magisk_alt_repo")) { if (repoData != null) { - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getExistingKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().name("ReposList.realm").encryptionKey(MainApplication.getINSTANCE().getKey()).allowQueriesOnUiThread(true).allowWritesOnUiThread(true).directory(MainApplication.getINSTANCE().getDataDirWithPath("realms")).schemaVersion(1).build(); Realm realm = Realm.getInstance(realmConfiguration); RealmResults repoDataRealmResults = realm.where(ReposList.class).equalTo("id", repoData.id).findAll(); Timber.d("Setting preference " + preferenceName + " because it is not the Androidacy repo or the Magisk Alt Repo"); diff --git a/app/src/main/java/com/fox2code/mmm/utils/TimberUtils.kt b/app/src/main/java/com/fox2code/mmm/utils/TimberUtils.kt new file mode 100644 index 0000000..26e6ba0 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/utils/TimberUtils.kt @@ -0,0 +1,28 @@ +package com.fox2code.mmm.utils +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.MainApplication.ReleaseTree +import io.sentry.Sentry +import io.sentry.SentryLevel +import io.sentry.android.timber.SentryTimberTree +import timber.log.Timber +import timber.log.Timber.Forest.plant + +@Suppress("UnstableApiUsage") +object TimberUtils { + + @JvmStatic + fun configTimber() { + // init timber + // init timber + if (BuildConfig.DEBUG) { + plant(Timber.DebugTree()) + } else { + if (MainApplication.isCrashReportingEnabled()) { + plant(SentryTimberTree(Sentry.getCurrentHub(), SentryLevel.ERROR, SentryLevel.ERROR)) + } else { + plant(ReleaseTree()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.java b/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.java index 7c4b047..5cf2405 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.java @@ -29,6 +29,7 @@ import com.fox2code.mmm.utils.io.Files; import com.google.android.material.snackbar.Snackbar; import com.google.net.cronet.okhttptransport.CronetInterceptor; +import org.apache.commons.io.FileUtils; import org.chromium.net.CronetEngine; import java.io.ByteArrayOutputStream; @@ -37,6 +38,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; import java.net.Proxy; +import java.net.URL; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -247,6 +249,8 @@ public enum Http { doh = MainApplication.isDohEnabled(); } + private static boolean urlFactoryInstalled; + private static OkHttpClient.Builder followRedirects(OkHttpClient.Builder builder, boolean followRedirects) { return builder.followRedirects(followRedirects).followSslRedirects(followRedirects); } @@ -411,50 +415,23 @@ public enum Http { return hasWebView; } - public static void ensureCacheDirs(MainActivity mainActivity) { - // Recursively ensure cache dirs for webview exist under our cache dir - File cacheDir = mainActivity.getCacheDir(); - File webviewCacheDir = new File(cacheDir, "WebView"); - if (!webviewCacheDir.exists()) { - if (!webviewCacheDir.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } - } - File webviewCacheDirCache = new File(webviewCacheDir, "Default"); - if (!webviewCacheDirCache.exists()) { - if (!webviewCacheDirCache.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } - } - File webviewCacheDirCacheCodeCache = new File(webviewCacheDirCache, "HTTP Cache"); - if (!webviewCacheDirCacheCodeCache.exists()) { - if (!webviewCacheDirCacheCodeCache.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } - } - File webviewCacheDirCacheCodeCacheIndex = new File(webviewCacheDirCacheCodeCache, "Code Cache"); - if (!webviewCacheDirCacheCodeCacheIndex.exists()) { - if (!webviewCacheDirCacheCodeCacheIndex.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } - } - File webviewCacheDirCacheCodeCacheIndexIndex = new File(webviewCacheDirCacheCodeCacheIndex, "Index"); - if (!webviewCacheDirCacheCodeCacheIndexIndex.exists()) { - if (!webviewCacheDirCacheCodeCacheIndexIndex.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } - } - // Create the js and wasm dirs - File webviewCacheDirCacheCodeCacheIndexIndexJs = new File(webviewCacheDirCacheCodeCache, "js"); - if (!webviewCacheDirCacheCodeCacheIndexIndexJs.exists()) { - if (!webviewCacheDirCacheCodeCacheIndexIndexJs.mkdirs()) { - Timber.e("Failed to create webview cache dir"); - } + public static void ensureCacheDirs() { + try { + FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/wasm").replaceAll("//", "/"))); + FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/WebView/Default/HTTP Cache/Code Cache/js").replaceAll("//", "/"))); + FileUtils.forceMkdir(new File((MainApplication.getINSTANCE().getDataDir() + "/cache/cronet").replaceAll("//", "/"))); + } catch (IOException e) { + Timber.e("Could not create cache dirs"); } - File webviewCacheDirCacheCodeCacheIndexIndexWasm = new File(webviewCacheDirCacheCodeCache, "wasm"); - if (!webviewCacheDirCacheCodeCacheIndexIndexWasm.exists()) { - if (!webviewCacheDirCacheCodeCacheIndexIndexWasm.mkdirs()) { - Timber.e("Failed to create webview cache dir"); + } + + public static void ensureURLHandler(Context context) { + if (!urlFactoryInstalled) { + try { + URL.setURLStreamHandlerFactory(new CronetEngine.Builder(context).build().createURLStreamHandlerFactory()); + urlFactoryInstalled = true; + } catch (Error ignored) { + // Ignore } } } diff --git a/app/src/main/res/drawable/baseline_arrow_back_24.xml b/app/src/main/res/drawable/baseline_arrow_back_24.xml new file mode 100644 index 0000000..9d84252 --- /dev/null +++ b/app/src/main/res/drawable/baseline_arrow_back_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index 8b2f384..d21806a 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -101,7 +101,8 @@ @@ -109,7 +110,7 @@ android:id="@+id/setup_androidacy_repo" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="false" android:key="pref_androidacy_repo_enabled" android:text="@string/setup_androidacy_repo" @@ -119,7 +120,7 @@ @@ -163,7 +165,7 @@ android:id="@+id/setup_crash_reporting" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="false" android:key="pref_crash_reporting_enabled" android:text="@string/setup_crash_reporting" @@ -184,7 +186,7 @@ android:id="@+id/setup_crash_reporting_pii" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="false" android:key="pref_crash_reporting_pii" android:text="@string/setup_crash_reporting_pii" @@ -207,7 +209,7 @@ android:id="@+id/setup_app_analytics" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="false" android:key="pref_app_analytics" android:text="@string/setup_app_analytics" @@ -225,7 +227,8 @@ @@ -233,7 +236,7 @@ android:id="@+id/setup_background_update_check" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="false" android:key="pref_background_update_check" android:text="@string/setup_background_update_check" @@ -255,7 +258,7 @@ android:id="@+id/setup_background_update_check_require_wifi" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_margin="4dp" + android:layout_margin="5dp" android:checked="true" android:key="pref_background_update_check_require_wifi" android:text="@string/setup_background_update_check_require_wifi" @@ -276,7 +279,8 @@ diff --git a/app/src/main/res/layout/installer.xml b/app/src/main/res/layout/installer.xml index 97f9bd1..66656db 100644 --- a/app/src/main/res/layout/installer.xml +++ b/app/src/main/res/layout/installer.xml @@ -36,16 +36,10 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - + app:layout_constraintBottom_toBottomOf="parent" + app:menu="@menu/bottom_nav_install" /> + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_install.xml b/app/src/main/res/menu/bottom_nav_install.xml new file mode 100644 index 0000000..babc1e8 --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_install.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 341373c..52e5a91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -406,4 +406,5 @@ Allow us to track app usage and installs. Fully GDPR compliant and uses Matomo, hosted by Androidacy. Debugging News and updates + Go back