From 584d8b126a4fddddfa22ff2b260c42ce01cb0c16 Mon Sep 17 00:00:00 2001 From: Fox2Code Date: Sun, 23 Jan 2022 19:17:08 +0100 Subject: [PATCH] 0.2.7 Release --- DEVELOPERS.md | 14 ++- README.md | 2 + app/build.gradle | 7 +- app/src/main/AndroidManifest.xml | 3 +- .../com/fox2code/mmm/ActionButtonType.java | 10 +- .../com/fox2code/mmm/AppUpdateManager.java | 4 +- .../main/java/com/fox2code/mmm/Constants.java | 2 + .../java/com/fox2code/mmm/MainActivity.java | 40 +++++- .../com/fox2code/mmm/MainApplication.java | 37 +++--- .../java/com/fox2code/mmm/ModuleHolder.java | 23 ++-- .../com/fox2code/mmm/ModuleViewAdapter.java | 48 ++++++-- .../fox2code/mmm/ModuleViewListBuilder.java | 9 +- .../com/fox2code/mmm/NotificationType.java | 2 +- .../fox2code/mmm/compat/CompatActivity.java | 18 ++- .../mmm/compat/CompatThemeWrapper.java | 1 - .../mmm/installer/InstallerActivity.java | 114 +++++++++++++---- .../mmm/installer/InstallerTerminal.java | 2 - .../fox2code/mmm/manager/LocalModuleInfo.java | 45 +++++++ .../com/fox2code/mmm/manager/ModuleInfo.java | 18 +++ .../fox2code/mmm/manager/ModuleManager.java | 72 +++++++++-- .../java/com/fox2code/mmm/repo/RepoData.java | 115 ++++++++++++++++-- .../com/fox2code/mmm/repo/RepoManager.java | 29 ++++- .../com/fox2code/mmm/repo/RepoUpdater.java | 1 + .../mmm/settings/SettingsActivity.java | 20 +-- .../com/fox2code/mmm/utils/FastException.java | 15 +++ .../java/com/fox2code/mmm/utils/Http.java | 52 +++++++- .../com/fox2code/mmm/utils/PropUtils.java | 57 +++++++-- .../drawable/ic_baseline_access_time_24.xml | 13 ++ .../drawable/ic_baseline_sort_by_alpha_24.xml | 10 ++ app/src/main/res/values/strings.xml | 22 +++- app/src/main/res/values/themes.xml | 2 +- app/src/main/res/xml/root_preferences.xml | 23 ++-- build.gradle | 2 +- 33 files changed, 678 insertions(+), 154 deletions(-) create mode 100644 app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java create mode 100644 app/src/main/java/com/fox2code/mmm/utils/FastException.java create mode 100644 app/src/main/res/drawable/ic_baseline_access_time_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_sort_by_alpha_24.xml diff --git a/DEVELOPERS.md b/DEVELOPERS.md index 47c3f42..0a5cab3 100644 --- a/DEVELOPERS.md +++ b/DEVELOPERS.md @@ -55,7 +55,7 @@ So the original module maker can still override them The Fox's Mmm also allow better control over it's installer interface -Fox's Mmm defined the variable `MMM_EXT_SUPPORT` to expose it's extension support +Fox's Mmm define the variable `MMM_EXT_SUPPORT` to expose it's extensions support All the commands start with it `#!`, by default the manager process command as log output unless `#!useExt` is sent to indicate that the app is ready to use commands @@ -70,15 +70,21 @@ Commands: - `clearTerminal`: Clear the terminal of any text, making it empty - `scrollUp`: Scroll up at the top of the terminal - `scrollDown`: Scroll down at the bottom of the terminal -- `showLoading`: Show an indeterminate progress bar - (Note: the bar is automatically hidden when the install finish) +- `showLoading `: Show an indeterminate progress bar + (Note: Status bar is indeterminate if 0 is provided) +- `setLoading `: Set loading progress if the bar is not indeterminate. - `hideLoading`: Hide the indeterminate progress bar if previously shown - `setSupportLink `: Set support link to show when the install finish (Note: Modules installed from repo will not show the config button if a link is set) +Variables: +- `MMM_EXT_SUPPORT` declared if extensions are supported +- `MMM_USER_LANGUAGE` the current user selected language +- `MMM_APP_VERSION` display version of the app (Ex: `x.y.z`) + Note: The current behavior with unknown command is to ignore them, -I may add or remove commands in the future depending of how they are used +I may add or remove commands/variables in the future depending of how they are used A wrapper script to use theses commands could be ```sh diff --git a/README.md b/README.md index c98095a..d15428f 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ The app currently use these two repo as it's module sources, with it's benefits [https://github.com/Magisk-Modules-Repo](https://github.com/Magisk-Modules-Repo) - No longer accept new modules or update to existing modules + (Fox's MMM use a workaround to get latest version of modules, because the + method used by the official Magisk app give outdated versions of the modules) - May be shut down at any moment - Official app dropped support for it - Officially supported by Fox's mmm diff --git a/app/build.gradle b/app/build.gradle index 04f4781..a409bb7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "com.fox2code.mmm" minSdk 21 targetSdk 32 - versionCode 17 - versionName "0.2.6" + versionCode 18 + versionName "0.2.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -31,8 +31,7 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } - - lintOptions { + lint { disable 'MissingTranslation' } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 70ca125..f742f4d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,6 +28,7 @@ android:theme="@style/Theme.MagiskModuleManager" android:fullBackupContent="@xml/full_backup_content" android:dataExtractionRules="@xml/data_extraction_rules" + android:usesCleartextTraffic="false" tools:targetApi="s"> @@ -39,7 +40,7 @@ android:name=".settings.SettingsActivity" android:parentActivityName=".MainActivity" android:exported="true" - android:label="@string/title_activity_settings" > + android:label="@string/title_activity_settings"> diff --git a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java index 2e7171a..a3786f8 100644 --- a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java @@ -43,10 +43,12 @@ public enum ActionButtonType { @Override public void doAction(ImageButton button, ModuleHolder moduleHolder) { - RepoModule repoModule = moduleHolder.repoModule; - if (repoModule == null) return; - IntentHelper.openInstaller(button.getContext(), repoModule.zipUrl, - repoModule.moduleInfo.name, repoModule.moduleInfo.config); + ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); + if (moduleInfo == null) return; + String updateZipUrl = moduleHolder.getUpdateZipUrl(); + if (updateZipUrl == null) return; + IntentHelper.openInstaller(button.getContext(), updateZipUrl, + moduleInfo.name, moduleInfo.config); } }, UNINSTALL() { diff --git a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java index b601f45..8ecb191 100644 --- a/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java +++ b/app/src/main/java/com/fox2code/mmm/AppUpdateManager.java @@ -41,8 +41,8 @@ public class AppUpdateManager { return true; long lastChecked = this.lastChecked; if (lastChecked != 0 && - // Avoid spam calls by putting a 10 seconds timer - lastChecked < System.currentTimeMillis() - 10000L) + // Avoid spam calls by putting a 60 seconds timer + lastChecked < System.currentTimeMillis() - 60000L) return force && this.peekShouldUpdate(); synchronized (this.updateLock) { if (lastChecked != this.lastChecked) diff --git a/app/src/main/java/com/fox2code/mmm/Constants.java b/app/src/main/java/com/fox2code/mmm/Constants.java index 674a074..4ac56cd 100644 --- a/app/src/main/java/com/fox2code/mmm/Constants.java +++ b/app/src/main/java/com/fox2code/mmm/Constants.java @@ -11,6 +11,8 @@ public class Constants { public static final String EXTRA_INSTALL_PATH = "extra_install_path"; public static final String EXTRA_INSTALL_NAME = "extra_install_name"; public static final String EXTRA_INSTALL_CONFIG = "extra_install_config"; + public static final String EXTRA_INSTALL_NO_PATCH = "extra_install_no_patch"; + public static final String EXTRA_INSTALL_NO_EXTENSIONS = "extra_install_no_extensions"; public static final String EXTRA_MARKDOWN_URL = "extra_markdown_url"; public static final String EXTRA_MARKDOWN_TITLE = "extra_markdown_title"; public static final String EXTRA_MARKDOWN_CONFIG = "extra_markdown_config"; diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index d859744..4efc371 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -18,6 +18,7 @@ import android.widget.TextView; import com.fox2code.mmm.compat.CompatActivity; import com.fox2code.mmm.installer.InstallerInitializer; +import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.settings.SettingsActivity; @@ -115,16 +116,42 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O progressIndicator.setMax(PRECISION); }); Log.i(TAG, "Scanning for modules!"); - RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> - progressIndicator.setProgressCompat((int) (value * PRECISION), true))); + final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount(); + RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> + progressIndicator.setProgressCompat( + (int) (value * PRECISION), true) :() -> + progressIndicator.setProgressCompat( + (int) (value * PRECISION * 0.75F), true))); + if (!RepoManager.getINSTANCE().hasConnectivity()) { + moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); + } else { + if (AppUpdateManager.getAppUpdateManager().checkUpdate(true)) + moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); + if (max != 0) { + int current = 0; + for (LocalModuleInfo localModuleInfo : + ModuleManager.getINSTANCE().getModules().values()) { + if (localModuleInfo.updateJson != null) { + try { + localModuleInfo.checkModuleUpdate(); + } catch (Exception e) { + Log.e("MainActivity", "Failed to fetch update of: " + + localModuleInfo.id, e); + } + current++; + final int currentTmp = current; + runOnUiThread(() -> progressIndicator.setProgressCompat( + (int) ((1F * currentTmp / max) * PRECISION * 0.25F + + (PRECISION * 0.75F)), true)); + } + } + } + } runOnUiThread(() -> { + progressIndicator.setProgressCompat(PRECISION, true); progressIndicator.setVisibility(View.GONE); searchView.setEnabled(true); }); - if (!RepoManager.getINSTANCE().hasConnectivity()) - moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); - else if (AppUpdateManager.getAppUpdateManager().checkUpdate(true)) - moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); moduleViewListBuilder.appendRemoteModules(); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); Log.i(TAG, "Finished app opening state!"); @@ -156,6 +183,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O this.cardIconifyUpdate(); this.moduleViewListBuilder.setQuery(null); Log.i(TAG, "Item After"); + this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter); InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() { @Override public void onPathReceived(String path) { diff --git a/app/src/main/java/com/fox2code/mmm/MainApplication.java b/app/src/main/java/com/fox2code/mmm/MainApplication.java index 624d1cd..9cd2f85 100644 --- a/app/src/main/java/com/fox2code/mmm/MainApplication.java +++ b/app/src/main/java/com/fox2code/mmm/MainApplication.java @@ -212,10 +212,13 @@ public class MainApplication extends Application implements CompatActivity.Appli switch (this.managerThemeResId) { case R.style.Theme_MagiskModuleManager: this.nightModeOverride = null; + break; case R.style.Theme_MagiskModuleManager_Light: this.nightModeOverride = Boolean.FALSE; + break; case R.style.Theme_MagiskModuleManager_Dark: this.nightModeOverride = Boolean.TRUE; + break; default: } if (this.markwonThemeContext != null) { @@ -225,6 +228,25 @@ public class MainApplication extends Application implements CompatActivity.Appli this.markwon = null; } + public void updateTheme() { + @StyleRes int themeResId; + String theme; + switch (theme = getSharedPreferences().getString("pref_theme", "system")) { + default: + Log.w("MainApplication", "Unknown theme id: " + theme); + case "system": + themeResId = R.style.Theme_MagiskModuleManager; + break; + case "dark": + themeResId = R.style.Theme_MagiskModuleManager_Dark; + break; + case "light": + themeResId = R.style.Theme_MagiskModuleManager_Light; + break; + } + this.setManagerThemeResId(themeResId); + } + @StyleRes public int getManagerThemeResId() { return managerThemeResId; @@ -267,20 +289,7 @@ public class MainApplication extends Application implements CompatActivity.Appli } else { MainApplication.firstBoot = bootPrefs.getBoolean("first_boot", false); } - @StyleRes int themeResId; - switch (getSharedPreferences().getString("pref_theme", "system")) { - default: - case "system": - themeResId = R.style.Theme_MagiskModuleManager; - break; - case "dark": - themeResId = R.style.Theme_MagiskModuleManager_Dark; - break; - case "light": - themeResId = R.style.Theme_MagiskModuleManager_Light; - break; - } - this.setManagerThemeResId(themeResId); + this.updateTheme(); // Update SSL Ciphers if update is possible GMSProviderInstaller.installIfNeeded(this); // Update emoji config diff --git a/app/src/main/java/com/fox2code/mmm/ModuleHolder.java b/app/src/main/java/com/fox2code/mmm/ModuleHolder.java index 49c553f..8cdd267 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleHolder.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleHolder.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.StringRes; import com.fox2code.mmm.installer.InstallerInitializer; +import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.repo.RepoModule; import com.fox2code.mmm.utils.IntentHelper; @@ -24,7 +25,7 @@ public final class ModuleHolder implements Comparable { public final NotificationType notificationType; public final Type separator; public final int footerPx; - public ModuleInfo moduleInfo; + public LocalModuleInfo moduleInfo; public RepoModule repoModule; public ModuleHolder(String moduleId) { @@ -60,7 +61,15 @@ public final class ModuleHolder implements Comparable { } public ModuleInfo getMainModuleInfo() { - return this.repoModule != null ? this.repoModule.moduleInfo : this.moduleInfo; + return this.repoModule != null && (this.moduleInfo == null || + this.moduleInfo.versionCode < this.repoModule.moduleInfo.versionCode) + ? this.repoModule.moduleInfo : this.moduleInfo; + } + + public String getUpdateZipUrl() { + return this.moduleInfo == null || (this.repoModule != null && + this.moduleInfo.updateVersionCode < this.repoModule.lastUpdated) ? + this.repoModule.zipUrl : this.moduleInfo.updateZipUrl; } public String getMainModuleName() { @@ -103,10 +112,9 @@ public final class ModuleHolder implements Comparable { return Type.NOTIFICATION; } else if (this.moduleInfo == null) { return Type.INSTALLABLE; - } else if (this.repoModule == null) { - return Type.INSTALLED; - } else if (this.moduleInfo.versionCode < - this.repoModule.moduleInfo.versionCode) { + } else if (this.moduleInfo.versionCode < this.moduleInfo.updateVersionCode || + (this.repoModule != null && this.moduleInfo.versionCode < + this.repoModule.moduleInfo.versionCode)) { return Type.UPDATABLE; } else { return Type.INSTALLED; @@ -139,7 +147,8 @@ public final class ModuleHolder implements Comparable { if (this.repoModule != null) { buttonTypeList.add(ActionButtonType.INFO); } - if (this.repoModule != null && !showcaseMode && + if ((this.repoModule != null || (this.moduleInfo != null && + this.moduleInfo.updateZipUrl != null)) && !showcaseMode && InstallerInitializer.peekMagiskPath() != null) { buttonTypeList.add(ActionButtonType.UPDATE_INSTALL); } diff --git a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java index 9c70f15..02716ca 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java @@ -19,6 +19,7 @@ import androidx.annotation.StringRes; import androidx.cardview.widget.CardView; import androidx.recyclerview.widget.RecyclerView; +import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.repo.RepoModule; @@ -164,7 +165,7 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter + localModuleInfo.updateVersionCode) { + this.creditText.setText((localModuleInfo == null || + moduleInfo.version.equals(localModuleInfo.version) ? + moduleInfo.version : localModuleInfo.version + " (" + + this.getString(R.string.module_last_update) + + moduleInfo.version + ")") + " " + + this.getString(R.string.module_by) + " " + moduleInfo.author); + } else { + this.creditText.setText(( + localModuleInfo.version.equals(localModuleInfo.updateVersion) ? + localModuleInfo.version : localModuleInfo.version + " (" + + this.getString(R.string.module_last_update) + + localModuleInfo.updateVersion + ")") + " " + + this.getString(R.string.module_by) + " " + localModuleInfo.author); + } + if (moduleInfo.description == null || moduleInfo.description.isEmpty()) { + this.descriptionText.setText(R.string.no_desc_found); + } else { + this.descriptionText.setText(moduleInfo.description); + } String updateText = moduleHolder.getUpdateTimeText(); if (!updateText.isEmpty()) { this.updateText.setVisibility(View.VISIBLE); @@ -262,20 +278,32 @@ public final class ModuleViewAdapter extends RecyclerView.Adapter { Log.i(TAG, "A1: " + moduleManager.getModules().size()); - for (ModuleInfo moduleInfo : moduleManager.getModules().values()) { + for (LocalModuleInfo moduleInfo : moduleManager.getModules().values()) { ModuleHolder moduleHolder = this.mappedModuleHolders.get(moduleInfo.id); if (moduleHolder == null) { this.mappedModuleHolders.put(moduleInfo.id, @@ -190,6 +191,12 @@ public class ModuleViewListBuilder { }); } + public void refreshNotificationsUI(ModuleViewAdapter moduleViewAdapter) { + final int notificationCount = this.notifications.size(); + notifySizeChanged(moduleViewAdapter, 0, + notificationCount, notificationCount); + } + private boolean matchFilter(ModuleHolder moduleHolder) { if (this.query.isEmpty()) return true; ModuleInfo moduleInfo = moduleHolder.getMainModuleInfo(); diff --git a/app/src/main/java/com/fox2code/mmm/NotificationType.java b/app/src/main/java/com/fox2code/mmm/NotificationType.java index edf4793..3c0aa28 100644 --- a/app/src/main/java/com/fox2code/mmm/NotificationType.java +++ b/app/src/main/java/com/fox2code/mmm/NotificationType.java @@ -122,7 +122,7 @@ public enum NotificationType implements NotificationTypeCst { public final boolean special; NotificationType(@StringRes int textId, int iconId) { - this(textId, iconId, R.attr.colorError, R.attr.colorOnError); + this(textId, iconId, R.attr.colorError, R.attr.colorOnPrimary); //R.attr.colorOnError); } NotificationType(@StringRes int textId, int iconId, int backgroundAttr, int foregroundAttr) { diff --git a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java index 74b6cea..e5dcf23 100644 --- a/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java +++ b/app/src/main/java/com/fox2code/mmm/compat/CompatActivity.java @@ -7,6 +7,7 @@ import android.content.ContextWrapper; import android.content.Intent; import android.content.res.Resources; import android.os.Bundle; +import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -117,8 +118,13 @@ public class CompatActivity extends AppCompatActivity { } public void setDisplayHomeAsUpEnabled(boolean showHomeAsUp) { - androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar(); - + androidx.appcompat.app.ActionBar compatActionBar; + try { + compatActionBar = this.getSupportActionBar(); + } catch (Exception e) { + Log.e(TAG, "Failed to call getSupportActionBar", e); + compatActionBar = null; // Allow fallback to builtin actionBar. + } if (compatActionBar != null) { compatActionBar.setDisplayHomeAsUpEnabled(showHomeAsUp); } else { @@ -192,7 +198,13 @@ public class CompatActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { - androidx.appcompat.app.ActionBar compatActionBar = this.getSupportActionBar(); + androidx.appcompat.app.ActionBar compatActionBar; + try { + compatActionBar = this.getSupportActionBar(); + } catch (Exception e) { + Log.e(TAG, "Failed to call getSupportActionBar", e); + compatActionBar = null; // Allow fallback to builtin actionBar. + } android.app.ActionBar actionBar = this.getActionBar(); if (compatActionBar != null ? (compatActionBar.getDisplayOptions() & androidx.appcompat.app.ActionBar.DISPLAY_HOME_AS_UP) != 0 : diff --git a/app/src/main/java/com/fox2code/mmm/compat/CompatThemeWrapper.java b/app/src/main/java/com/fox2code/mmm/compat/CompatThemeWrapper.java index abaf7c6..56b688f 100644 --- a/app/src/main/java/com/fox2code/mmm/compat/CompatThemeWrapper.java +++ b/app/src/main/java/com/fox2code/mmm/compat/CompatThemeWrapper.java @@ -1,6 +1,5 @@ package com.fox2code.mmm.compat; -import android.app.Activity; import android.content.Context; import android.content.res.Resources; 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 361708c..7338c0c 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -2,6 +2,7 @@ package com.fox2code.mmm.installer; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.os.Bundle; @@ -12,10 +13,12 @@ import android.view.WindowManager; import android.widget.Toast; import com.fox2code.mmm.ActionButtonType; +import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.Constants; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.compat.CompatActivity; +import com.fox2code.mmm.utils.FastException; import com.fox2code.mmm.utils.Files; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; @@ -48,6 +51,8 @@ public class InstallerActivity extends CompatActivity { final Intent intent = this.getIntent(); final String target; final String name; + final boolean noPatch; + final boolean noExtensions; // Should we allow 3rd part app to install modules? if (Constants.INTENT_INSTALL_INTERNAL.equals(intent.getAction())) { if (!MainApplication.checkSecret(intent)) { @@ -55,13 +60,17 @@ public class InstallerActivity extends CompatActivity { this.forceBackPressed(); return; } - target = intent.getExtras().getString(Constants.EXTRA_INSTALL_PATH); - name = intent.getExtras().getString(Constants.EXTRA_INSTALL_NAME); + target = intent.getStringExtra(Constants.EXTRA_INSTALL_PATH); + name = intent.getStringExtra(Constants.EXTRA_INSTALL_NAME); + noPatch = intent.getBooleanExtra(Constants.EXTRA_INSTALL_NO_PATCH, false); + noExtensions = intent.getBooleanExtra(// Allow intent to disable extensions + Constants.EXTRA_INSTALL_NO_EXTENSIONS, false); } else { Toast.makeText(this, "Unknown intent!", Toast.LENGTH_SHORT).show(); this.forceBackPressed(); return; } + Log.i(TAG, "Install link: " + target); boolean urlMode = target.startsWith("http://") || target.startsWith("https://"); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); setTitle(name); @@ -103,23 +112,29 @@ public class InstallerActivity extends CompatActivity { this.progressIndicator.setProgressCompat(progress, true); }); }); - this.runOnUiThread(() -> { - this.installerTerminal.addLine("- Patching " + name); - this.progressIndicator.setVisibility(View.GONE); - this.progressIndicator.setIndeterminate(true); - }); - Log.i(TAG, "Patching: " + moduleCache.getName()); - try (OutputStream outputStream = new FileOutputStream(moduleCache)) { - Files.patchModuleSimple(rawModule, outputStream); - outputStream.flush(); - } finally { - //noinspection UnusedAssignment (Important for GC) - rawModule = null; + if (noPatch) { + try (OutputStream outputStream = new FileOutputStream(moduleCache)) { + outputStream.write(rawModule); + outputStream.flush(); + } + } else { + this.runOnUiThread(() -> { + this.installerTerminal.addLine("- Patching " + name); + this.progressIndicator.setVisibility(View.GONE); + this.progressIndicator.setIndeterminate(true); + }); + Log.i(TAG, "Patching: " + moduleCache.getName()); + try (OutputStream outputStream = new FileOutputStream(moduleCache)) { + Files.patchModuleSimple(rawModule, outputStream); + outputStream.flush(); + } } + //noinspection UnusedAssignment (Important to avoid OutOfMemoryError) + rawModule = null; this.runOnUiThread(() -> { this.installerTerminal.addLine("- Installing " + name); }); - this.doInstall(moduleCache); + this.doInstall(moduleCache, noExtensions); } catch (IOException e) { Log.e(TAG, "Failed to download module zip", e); this.setInstallStateFinished(false, @@ -129,24 +144,36 @@ public class InstallerActivity extends CompatActivity { } else { this.installerTerminal.addLine("- Installing " + name); new Thread(() -> this.doInstall( - this.toDelete = new File(target)), + this.toDelete = new File(target), noExtensions), "Install Thread").start(); } } - private void doInstall(File file) { + private void doInstall(File file,boolean noExtensions) { Log.i(TAG, "Installing: " + moduleCache.getName()); InstallerController installerController = new InstallerController( - this.progressIndicator, this.installerTerminal, file.getAbsoluteFile()); + this.progressIndicator, this.installerTerminal, + file.getAbsoluteFile(), noExtensions); InstallerMonitor installerMonitor; Shell.Job installJob; - if (MainApplication.isUsingMagiskCommand()) { + if (MainApplication.isUsingMagiskCommand() || noExtensions) { installerMonitor = new InstallerMonitor(new File(InstallerInitializer .peekMagiskPath().equals("/sbin") ? "/sbin/magisk" : "/system/bin/magisk")); - installJob = Shell.su("export MMM_EXT_SUPPORT=1", - "cd \"" + this.moduleCache.getAbsolutePath() + "\"", - "magisk --install-module \"" + file.getAbsolutePath() + "\"") - .to(installerController, installerMonitor); + if (noExtensions) { + installJob = Shell.su( // No Extensions + "cd \"" + this.moduleCache.getAbsolutePath() + "\"", + "magisk --install-module \"" + file.getAbsolutePath() + "\"") + .to(installerController, installerMonitor); + } else { + installJob = Shell.su("export MMM_EXT_SUPPORT=1", + "export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ? + "en-US" : Resources.getSystem() + .getConfiguration().locale.toLanguageTag()), + "export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME, + "cd \"" + this.moduleCache.getAbsolutePath() + "\"", + "magisk --install-module \"" + file.getAbsolutePath() + "\"") + .to(installerController, installerMonitor); + } } else { File installScript = this.extractCompatScript(); if (installScript == null) { @@ -156,6 +183,10 @@ public class InstallerActivity extends CompatActivity { } installerMonitor = new InstallerMonitor(installScript); installJob = Shell.su("export MMM_EXT_SUPPORT=1", + "export MMM_USER_LANGUAGE=" + (MainApplication.isForceEnglish() ? + "en-US" : Resources.getSystem() + .getConfiguration().locale.toLanguageTag()), + "export MMM_APP_VERSION=" + BuildConfig.VERSION_NAME, "cd \"" + this.moduleCache.getAbsolutePath() + "\"", "sh \"" + installScript.getAbsolutePath() + "\"" + " /dev/null 1 \"" + file.getAbsolutePath() + "\"") @@ -182,14 +213,17 @@ public class InstallerActivity extends CompatActivity { private final LinearProgressIndicator progressIndicator; private final InstallerTerminal terminal; private final File moduleFile; + private final boolean noExtension; private boolean enabled, useExt; private String supportLink = ""; private InstallerController(LinearProgressIndicator progressIndicator, - InstallerTerminal terminal,File moduleFile) { + InstallerTerminal terminal,File moduleFile, + boolean noExtension) { this.progressIndicator = progressIndicator; this.terminal = terminal; this.moduleFile = moduleFile; + this.noExtension = noExtension; this.enabled = true; this.useExt = false; } @@ -198,7 +232,7 @@ public class InstallerActivity extends CompatActivity { public void onAddElement(String s) { if (!this.enabled) return; Log.d(TAG, "MSG: " + s); - if ("#!useExt".equals(s)) { + if ("#!useExt".equals(s.trim()) && !this.noExtension) { this.useExt = true; return; } @@ -216,7 +250,7 @@ public class InstallerActivity extends CompatActivity { final String command; int i = rawCommand.indexOf(' '); if (i != -1) { - arg = rawCommand.substring(i + 1); + arg = rawCommand.substring(i + 1).trim(); command = rawCommand.substring(2, i); } else { arg = ""; @@ -239,8 +273,36 @@ public class InstallerActivity extends CompatActivity { this.terminal.scrollDown(); break; case "showLoading": + if (!arg.isEmpty()) { + try { + short s = Short.parseShort(arg); + if (s <= 0) throw FastException.INSTANCE; + this.progressIndicator.setMax(s); + this.progressIndicator.setIndeterminate(false); + } catch (Exception ignored) { + this.progressIndicator.setProgressCompat(0, true); + this.progressIndicator.setMax(100); + if (this.progressIndicator.getVisibility() == View.VISIBLE) { + this.progressIndicator.setVisibility(View.GONE); + } + this.progressIndicator.setIndeterminate(true); + } + } else if (!rawCommand.trim().equals("#!showLoading")) { + this.progressIndicator.setProgressCompat(0, true); + this.progressIndicator.setMax(100); + if (this.progressIndicator.getVisibility() == View.VISIBLE) { + this.progressIndicator.setVisibility(View.GONE); + } + this.progressIndicator.setIndeterminate(true); + } this.progressIndicator.setVisibility(View.VISIBLE); break; + case "setLoading": + try { + this.progressIndicator.setProgressCompat( + Short.parseShort(arg), true); + } catch (Exception ignored) {} + break; case "hideLoading": this.progressIndicator.setVisibility(View.GONE); break; diff --git a/app/src/main/java/com/fox2code/mmm/installer/InstallerTerminal.java b/app/src/main/java/com/fox2code/mmm/installer/InstallerTerminal.java index 5b8b84a..52b2b31 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerTerminal.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerTerminal.java @@ -1,8 +1,6 @@ package com.fox2code.mmm.installer; -import android.graphics.Color; import android.graphics.Typeface; -import android.graphics.drawable.ColorDrawable; import android.view.ViewGroup; import android.widget.TextView; diff --git a/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java b/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java new file mode 100644 index 0000000..0fe70f5 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/manager/LocalModuleInfo.java @@ -0,0 +1,45 @@ +package com.fox2code.mmm.manager; + +import android.util.Log; + +import com.fox2code.mmm.utils.FastException; +import com.fox2code.mmm.utils.Http; + +import org.json.JSONObject; + +import java.nio.charset.StandardCharsets; + +public class LocalModuleInfo extends ModuleInfo { + public String updateVersion; + public long updateVersionCode = Long.MIN_VALUE; + public String updateZipUrl; + public String updateChangeLog; + + public LocalModuleInfo(String id) { + super(id); + } + + public void checkModuleUpdate() { + if (this.updateJson != null) { + try { + JSONObject jsonUpdate = new JSONObject(new String(Http.doHttpGet( + this.updateJson, false), StandardCharsets.UTF_8)); + this.updateVersion = jsonUpdate.optString("version"); + this.updateVersionCode = jsonUpdate.getLong("versionCode"); + this.updateZipUrl = jsonUpdate.getString("zipUrl"); + this.updateChangeLog = jsonUpdate.optString("changelog"); + if (this.updateZipUrl.isEmpty()) throw FastException.INSTANCE; + if (this.updateVersion == null || this.updateVersion.trim().isEmpty()) { + this.updateVersion = "v" + this.updateVersionCode; + } + } catch (Exception e) { + this.updateVersion = null; + this.updateVersionCode = Long.MIN_VALUE; + this.updateZipUrl = null; + this.updateChangeLog = null; + Log.w("LocalModuleInfo", + "Failed update checking for module: " + this.id, e); + } + } + } +} diff --git a/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java b/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java index 79adde6..c9dad2c 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleInfo.java @@ -21,6 +21,7 @@ public class ModuleInfo { public long versionCode; public String author; public String description; + public String updateJson; // Community meta public String support; public String donate; @@ -37,6 +38,23 @@ public class ModuleInfo { this.name = id; } + public ModuleInfo(ModuleInfo moduleInfo) { + this.id = moduleInfo.id; + this.name = moduleInfo.name; + this.version = moduleInfo.version; + this.versionCode = moduleInfo.versionCode; + this.author = moduleInfo.author; + this.description = moduleInfo.description; + this.updateJson = moduleInfo.updateJson; + this.support = moduleInfo.support; + this.donate = moduleInfo.donate; + this.config = moduleInfo.config; + this.minMagisk = moduleInfo.minMagisk; + this.minApi = moduleInfo.minApi; + this.maxApi = moduleInfo.maxApi; + this.flags = moduleInfo.flags; + } + public boolean hasFlag(int flag) { return (this.flags & flag) != 0; } 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 f54d719..a1fea85 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java @@ -4,10 +4,19 @@ import android.content.SharedPreferences; import android.util.Log; import com.fox2code.mmm.MainApplication; +import com.fox2code.mmm.R; +import com.fox2code.mmm.utils.Files; import com.fox2code.mmm.utils.PropUtils; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.io.SuFile; - +import com.topjohnwu.superuser.io.SuFileInputStream; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Iterator; @@ -20,9 +29,10 @@ public final class ModuleManager { ModuleInfo.FLAG_MODULE_DISABLED | ModuleInfo.FLAG_MODULE_UPDATING | ModuleInfo.FLAG_MODULE_UNINSTALLING | ModuleInfo.FLAG_MODULE_ACTIVE; private static final int FLAGS_RESET_UPDATE = FLAG_MM_INVALID | FLAG_MM_UNPROCESSED; - private final HashMap moduleInfos; + private final HashMap moduleInfos; private final SharedPreferences bootPrefs; private final Object scanLock = new Object(); + private int updatableModuleCount = 0; private boolean scanning; private static final ModuleManager INSTANCE = new ModuleManager(); @@ -75,16 +85,18 @@ public final class ModuleManager { v.version = null; v.versionCode = 0; v.author = null; - v.description = "No description found."; + v.description = ""; v.support = null; v.config = null; } String[] modules = new SuFile("/data/adb/modules").list(); if (modules != null) { for (String module : modules) { - ModuleInfo moduleInfo = moduleInfos.get(module); + if (!new SuFile("/data/adb/modules/" + module).isDirectory()) + continue; // Ignore non directory files inside modules folder + LocalModuleInfo moduleInfo = moduleInfos.get(module); if (moduleInfo == null) { - moduleInfo = new ModuleInfo(module); + moduleInfo = new LocalModuleInfo(module); moduleInfos.put(module, moduleInfo); // Shis should not really happen, but let's handles theses cases anyway moduleInfo.flags |= ModuleInfo.FLAG_MODULE_UPDATING_ONLY; @@ -119,9 +131,9 @@ public final class ModuleManager { String[] modules_update = new SuFile("/data/adb/modules_update").list(); if (modules_update != null) { for (String module : modules_update) { - ModuleInfo moduleInfo = moduleInfos.get(module); + LocalModuleInfo moduleInfo = moduleInfos.get(module); if (moduleInfo == null) { - moduleInfo = new ModuleInfo(module); + moduleInfo = new LocalModuleInfo(module); moduleInfos.put(module, moduleInfo); } moduleInfo.flags &= ~FLAGS_RESET_UPDATE; @@ -135,19 +147,28 @@ public final class ModuleManager { } } } - Iterator moduleInfoIterator = + this.updatableModuleCount = 0; + Iterator moduleInfoIterator = this.moduleInfos.values().iterator(); while (moduleInfoIterator.hasNext()) { - ModuleInfo moduleInfo = moduleInfoIterator.next(); + LocalModuleInfo moduleInfo = moduleInfoIterator.next(); if ((moduleInfo.flags & FLAG_MM_UNPROCESSED) != 0) { moduleInfoIterator.remove(); continue; // Don't process fallbacks if unreferenced } + if (moduleInfo.updateJson != null) { + this.updatableModuleCount++; + } else { + moduleInfo.updateVersion = null; + moduleInfo.updateVersionCode = Long.MIN_VALUE; + moduleInfo.updateZipUrl = null; + moduleInfo.updateChangeLog = null; + } if (moduleInfo.name == null || (moduleInfo.name.equals(moduleInfo.id))) { moduleInfo.name = Character.toUpperCase(moduleInfo.id.charAt(0)) + moduleInfo.id.substring(1).replace('_', ' '); } - if (moduleInfo.version == null) { + if (moduleInfo.version == null || moduleInfo.version.trim().isEmpty()) { moduleInfo.version = "v" + moduleInfo.versionCode; } } @@ -157,11 +178,16 @@ public final class ModuleManager { } } - public HashMap getModules() { + public HashMap getModules() { this.afterScan(); return this.moduleInfos; } + public int getUpdatableModuleCount() { + this.afterScan(); + return this.updatableModuleCount; + } + public boolean setEnabledState(ModuleInfo moduleInfo, boolean checked) { if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_UPDATING) && !checked) return false; SuFile disable = new SuFile("/data/adb/modules/" + moduleInfo.id + "/disable"); @@ -200,8 +226,28 @@ public final class ModuleManager { public boolean masterClear(ModuleInfo moduleInfo) { if (moduleInfo.hasFlag(ModuleInfo.FLAG_MODULE_ACTIVE)) return false; - Shell.su("rm -rf /data/adb/modules/" + moduleInfo.id + "/").exec(); - Shell.su("rm -rf /data/adb/modules_update/" + moduleInfo.id + "/").exec(); + String escapedId = moduleInfo.id.replace("\\", "\\\\") + .replace("\"", "\\\"").replace(" ", "\\ "); + try { // Check for module that declare having file outside their own folder. + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( + SuFileInputStream.open("/data/adb/modules/." + moduleInfo.id + "-files"), + StandardCharsets.UTF_8))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + line = line.trim().replace(' ', '.'); + if (!line.startsWith("/data/adb/") || line.contains("*") || + line.contains("/../") || line.endsWith("/..") || + line.startsWith("/data/adb/modules") || + line.equals("/data/adb/magisk.db")) continue; + line = line.replace("\\", "\\\\") + .replace("\"", "\\\""); + Shell.su("rm -rf \"" + line + "\"").exec(); + } + } + } catch (IOException ignored) {} + Shell.su("rm -rf /data/adb/modules/" + escapedId + "/").exec(); + Shell.su("rm -f /data/adb/modules/." + escapedId + "-files").exec(); + Shell.su("rm -rf /data/adb/modules_update/" + escapedId + "/").exec(); moduleInfo.flags = ModuleInfo.FLAG_METADATA_INVALID; return true; } 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 e0a59bb..457fc23 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -1,10 +1,12 @@ package com.fox2code.mmm.repo; +import android.annotation.SuppressLint; import android.content.SharedPreferences; -import android.text.TextUtils; +import android.util.Log; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.utils.Files; +import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.PropUtils; import org.json.JSONArray; @@ -12,11 +14,18 @@ import org.json.JSONException; import org.json.JSONObject; import java.io.File; +import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.Objects; public class RepoData { private final Object populateLock = new Object(); @@ -24,30 +33,64 @@ public class RepoData { public final File cacheRoot; public final SharedPreferences cachedPreferences; public final File metaDataCache; + public final boolean special; public final HashMap moduleHashMap; public long lastUpdate; public String name; + private final Map specialTimes; + private long specialLastUpdate; RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) { + this(url, cacheRoot, cachedPreferences, false); + } + + RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences,boolean special) { this.url = url; this.cacheRoot = cacheRoot; this.cachedPreferences = cachedPreferences; this.metaDataCache = new File(cacheRoot, "modules.json"); + this.special = special; this.moduleHashMap = new HashMap<>(); this.name = this.url; // Set url as default name + this.specialTimes = special ? new HashMap<>() : Collections.emptyMap(); if (!this.cacheRoot.isDirectory()) { this.cacheRoot.mkdirs(); - } else if (this.metaDataCache.exists()) { - try { - List modules = this.populate(new JSONObject( - new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8))); - for (RepoModule repoModule: modules) { - if (!this.tryLoadMetadata(repoModule)) { - repoModule.moduleInfo.flags &=~ ModuleInfo.FLAG_METADATA_INVALID; + } else { + if (special) { // Special times need to be loaded before populate + File metaDataCacheSpecial = new File(cacheRoot, "modules_times.json"); + if (metaDataCacheSpecial.exists()) { + try { + JSONArray jsonArray = new JSONArray(new String( + Files.read(this.metaDataCache), StandardCharsets.UTF_8)); + for (int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject = jsonArray.getJSONObject(i); + this.specialTimes.put(jsonObject.getString("name"), + Objects.requireNonNull(ISO_OFFSET_DATE_TIME.parse( + jsonObject.getString("pushed_at"))).getTime()); + Log.d("RepoData", "Got " + + jsonObject.getString("name") + " from local storage!"); + } + this.specialLastUpdate = metaDataCacheSpecial.lastModified(); + if (this.specialLastUpdate > System.currentTimeMillis()) { + this.specialLastUpdate = 0; // Don't allow time travel + } + } catch (Exception e) { + metaDataCacheSpecial.delete(); } } - } catch (Exception e) { - this.metaDataCache.delete(); + } + if (this.metaDataCache.exists()) { + try { + List modules = this.populate(new JSONObject( + new String(Files.read(this.metaDataCache), StandardCharsets.UTF_8))); + for (RepoModule repoModule : modules) { + if (!this.tryLoadMetadata(repoModule)) { + repoModule.moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID; + } + } + } catch (Exception e) { + this.metaDataCache.delete(); + } } } } @@ -62,6 +105,7 @@ public class RepoData { for (RepoModule repoModule : this.moduleHashMap.values()) { repoModule.processed = false; } + Log.d("RepoData", "Data: " + this.specialTimes.toString()); JSONArray array = jsonObject.getJSONArray("modules"); int len = array.length(); for (int i = 0; i < len; i++) { @@ -69,10 +113,23 @@ public class RepoData { String moduleId = module.getString("id"); // Deny remote modules ids shorter than 3 chars long or that start with a digit if (moduleId.length() < 3 || Character.isDigit(moduleId.charAt(0))) continue; + Long moduleLastUpdateSpecial = this.specialTimes.get(moduleId); long moduleLastUpdate = module.getLong("last_update"); String moduleNotesUrl = module.getString("notes_url"); String modulePropsUrl = module.getString("prop_url"); String moduleZipUrl = module.getString("zip_url"); + if (moduleLastUpdateSpecial != null) { // Fix last update time + Log.d("RepoData", "Data: " + moduleLastUpdate + " -> " + + moduleLastUpdateSpecial + " for " + moduleId); + moduleLastUpdate = Math.max(moduleLastUpdate, moduleLastUpdateSpecial); + moduleNotesUrl = Http.updateLink(moduleNotesUrl); + modulePropsUrl = Http.updateLink(modulePropsUrl); + moduleZipUrl = Http.updateLink(moduleZipUrl); + } else { + moduleNotesUrl = Http.fixUpLink(moduleNotesUrl); + modulePropsUrl = Http.fixUpLink(modulePropsUrl); + moduleZipUrl = Http.fixUpLink(moduleZipUrl); + } RepoModule repoModule = this.moduleHashMap.get(moduleId); if (repoModule == null) { repoModule = new RepoModule(moduleId); @@ -107,12 +164,17 @@ public class RepoData { return newModules; } + public void storeMetadata(RepoModule repoModule,byte[] data) throws IOException { + Files.write(new File(this.cacheRoot, repoModule.id + ".prop"), data); + } + public boolean tryLoadMetadata(RepoModule repoModule) { File file = new File(this.cacheRoot, repoModule.id + ".prop"); if (file.exists()) { try { ModuleInfo moduleInfo = repoModule.moduleInfo; - PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), false); + PropUtils.readProperties(moduleInfo, file.getAbsolutePath(), + repoModule.repoName + "/" + moduleInfo.name, false); moduleInfo.flags &= ~ModuleInfo.FLAG_METADATA_INVALID; if (moduleInfo.version == null) { moduleInfo.version = "v" + moduleInfo.versionCode; @@ -126,6 +188,37 @@ public class RepoData { return false; } + @SuppressLint("SimpleDateFormat") + private static final SimpleDateFormat ISO_OFFSET_DATE_TIME = + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + public void updateSpecialTimes(boolean force) throws IOException, JSONException { + if (!this.special) return; + synchronized (this.populateLock) { + if (this.specialLastUpdate == 0L || + (force && this.specialLastUpdate < System.currentTimeMillis() - 60000L)) { + File metaDataCacheSpecial = new File(cacheRoot, "modules_times.json"); + this.specialTimes.clear(); + try { + byte[] data = Http.doHttpGet( + "https://api.github.com/users/Magisk-Modules-Repo/repos", + false); + JSONArray jsonArray = new JSONArray(new String(data, StandardCharsets.UTF_8)); + for (int i = 0;i < jsonArray.length();i++) { + JSONObject jsonObject = jsonArray.optJSONObject(i); + this.specialTimes.put(jsonObject.getString("name"), + Objects.requireNonNull(ISO_OFFSET_DATE_TIME.parse( + jsonObject.getString("pushed_at"))).getTime()); + } + Files.write(metaDataCacheSpecial, data); + this.specialLastUpdate = System.currentTimeMillis(); + } catch (ParseException e) { + throw new IOException(e); + } + } + } + } + public String getNameOrFallback(String fallback) { return this.name == null || this.name.equals(this.url) ? diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java index 1683c45..a9f5c6e 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -22,11 +22,17 @@ public final class RepoManager { "https://magisk-modules-repo.github.io/submission/modules.json"; public static final String MAGISK_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Repo/submission/modules/modules.json"; + public static final String MAGISK_REPO_JSDELIVR = + "https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/submission@modules/modules.json"; public static final String MAGISK_ALT_REPO = "https://raw.githubusercontent.com/Magisk-Modules-Alt-Repo/json/main/modules.json"; + public static final String MAGISK_ALT_REPO_JSDELIVR = + "https://cdn.jsdelivr.net/gh/Magisk-Modules-Alt-Repo/json@main/modules.json"; - public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo"; - public static final String MAGISK_ALT_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Alt-Repo"; + public static final String MAGISK_REPO_HOMEPAGE = + "https://github.com/Magisk-Modules-Repo"; + public static final String MAGISK_ALT_REPO_HOMEPAGE = + "https://github.com/Magisk-Modules-Alt-Repo"; private static final Object lock = new Object(); private static RepoManager INSTANCE; @@ -56,8 +62,8 @@ public final class RepoManager { this.repoData = new LinkedHashMap<>(); this.modules = new HashMap<>(); // We do not have repo list config yet. - this.addRepoData(MAGISK_REPO); - this.addRepoData(MAGISK_ALT_REPO); + this.addRepoData(MAGISK_REPO_JSDELIVR); + this.addRepoData(MAGISK_ALT_REPO_JSDELIVR); // Populate default cache for (RepoData repoData:this.repoData.values()) { for (RepoModule repoModule:repoData.moduleHashMap.values()) { @@ -77,6 +83,14 @@ public final class RepoManager { } public RepoData get(String url) { + switch (url) { + case MAGISK_REPO_MANAGER: + case MAGISK_REPO: + url = MAGISK_REPO_JSDELIVR; + break; + case MAGISK_ALT_REPO: + url = MAGISK_ALT_REPO_JSDELIVR; + } return this.repoData.get(url); } @@ -154,6 +168,8 @@ public final class RepoManager { RepoData repoData = repoDatas[i]; for (RepoModule repoModule:repoModules) { try { + repoData.storeMetadata(repoModule, + Http.doHttpGet(repoModule.propUrl, false)); Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"), Http.doHttpGet(repoModule.propUrl, false)); if (repoDatas[i].tryLoadMetadata(repoModule) && (allowLowQualityModules || @@ -208,8 +224,10 @@ public final class RepoManager { switch (url) { case MAGISK_REPO_MANAGER: case MAGISK_REPO: + case MAGISK_REPO_JSDELIVR: return "magisk_repo"; case MAGISK_ALT_REPO: + case MAGISK_ALT_REPO_JSDELIVR: return "magisk_alt_repo"; default: return "repo_" + Hashes.hashSha1(url); @@ -221,7 +239,8 @@ public final class RepoManager { File cacheRoot = new File(this.mainApplication.getCacheDir(), id); SharedPreferences sharedPreferences = this.mainApplication .getSharedPreferences("mmm_" + id, Context.MODE_PRIVATE); - RepoData repoData = new RepoData(url, cacheRoot, sharedPreferences); + RepoData repoData = new RepoData(url, cacheRoot, + sharedPreferences, id.equals("magisk_repo")); this.repoData.put(url, repoData); return repoData; } 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 9ae9894..a8e133a 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -28,6 +28,7 @@ public class RepoUpdater { public int fetchIndex() { try { this.indexRaw = Http.doHttpGet(this.repoData.url, false); + if (this.repoData.special) this.repoData.updateSpecialTimes(true); this.toUpdate = this.repoData.populate(new JSONObject( new String(this.indexRaw, StandardCharsets.UTF_8))); // Since we reuse instances this should work 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 1c93a74..531991d 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -59,21 +59,11 @@ public class SettingsActivity extends CompatActivity { }); themePreference.setOnPreferenceChangeListener((preference, newValue) -> { devModeStep = 0; - @StyleRes int themeResId; - switch (String.valueOf(newValue)) { - default: - case "system": - themeResId = R.style.Theme_MagiskModuleManager; - break; - case "dark": - themeResId = R.style.Theme_MagiskModuleManager_Dark; - break; - case "light": - themeResId = R.style.Theme_MagiskModuleManager_Light; - break; - } - MainApplication.getINSTANCE().setManagerThemeResId(themeResId); - CompatActivity.getCompatActivity(this).setThemeRecreate(themeResId); + UiThreadHandler.handler.postDelayed(() -> { + MainApplication.getINSTANCE().updateTheme(); + CompatActivity.getCompatActivity(this).setThemeRecreate( + MainApplication.getINSTANCE().getManagerThemeResId()); + }, 1); return true; }); Preference forceEnglish = findPreference("pref_force_english"); diff --git a/app/src/main/java/com/fox2code/mmm/utils/FastException.java b/app/src/main/java/com/fox2code/mmm/utils/FastException.java new file mode 100644 index 0000000..4520ad6 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/utils/FastException.java @@ -0,0 +1,15 @@ +package com.fox2code.mmm.utils; + +import androidx.annotation.NonNull; + +public final class FastException extends RuntimeException { + public static final FastException INSTANCE = new FastException(); + + private FastException() {} + + @NonNull + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/app/src/main/java/com/fox2code/mmm/utils/Http.java b/app/src/main/java/com/fox2code/mmm/utils/Http.java index 58fca55..5c3e0ae 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -2,11 +2,14 @@ package com.fox2code.mmm.utils; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; import android.util.Log; import androidx.annotation.NonNull; +import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.MainApplication; +import com.fox2code.mmm.installer.InstallerInitializer; import java.io.ByteArrayOutputStream; import java.io.File; @@ -22,10 +25,12 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.concurrent.TimeUnit; import okhttp3.Cache; +import okhttp3.ConnectionSpec; import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.Dns; @@ -79,17 +84,30 @@ public class Http { Log.e(TAG, "Failed to init DoH", e); } httpclientBuilder.cookieJar(CookieJar.NO_COOKIES); + httpclientBuilder.addInterceptor(chain -> { + Request.Builder request = chain.request().newBuilder(); + if (InstallerInitializer.peekMagiskPath() != null) { + request.header("User-Agent", // Declare Magisk version to the server + "Magisk/" + InstallerInitializer.peekMagiskVersion()); + } + if (chain.request().header("Accept-Language") == null) { + request.header("Accept-Language", // Send system language to the server + Resources.getSystem().getConfiguration().locale.toLanguageTag()); + } + return chain.proceed(request.build()); + }); MainApplication mainApplication = MainApplication.getINSTANCE(); if (mainApplication != null) { + // Fallback DNS cache responses in case request fail but already succeeded once in the past httpclientBuilder.dns(fallbackDNS = new FallBackDNS(mainApplication, dns, "github.com", "api.github.com", "raw.githubusercontent.com", "camo.githubusercontent.com", "user-images.githubusercontent.com", "cdn.jsdelivr.net", "img.shields.io", "magisk-modules-repo.github.io", - "www.androidacy.com")); + "www.androidacy.com", "api.androidacy.com")); httpClient = httpclientBuilder.build(); httpclientBuilder.cache(new Cache( new File(mainApplication.getCacheDir(), "http_cache"), - 2L * 1024L * 1024L)); // 2Mib of cache + 16L * 1024L * 1024L)); // 16Mib of cache httpclientBuilder.cookieJar(new CDNCookieJar()); httpClientWithCache = httpclientBuilder.build(); Log.i(TAG, "Initialized Http successfully!"); @@ -316,4 +334,34 @@ public class Http { return inetAddresses; } } + + /** + * Change URL to appropriate url and force Magisk link to use latest version. + */ + public static String updateLink(String string) { + if (string.startsWith("https://cdn.jsdelivr.net/gh/Magisk-Modules-Repo/")) { + int start = string.lastIndexOf('@'), + end = string.lastIndexOf('/'); + if ((end - 8) <= start) return string; // Skip if not a commit id + return string.substring(0, start + 1) + "master" + string.substring(end); + } + if (string.startsWith("https://github.com/Magisk-Modules-Repo/")) { + int i = string.lastIndexOf("/archive/"); + if (i != -1) return string.substring(0, i + 9) + "master.zip"; + } + return fixUpLink(string); + } + + /** + * Change URL to appropriate url + */ + public static String fixUpLink(String string) { + if (string.startsWith("https://raw.githubusercontent.com/")) { + String[] tokens = string.substring(34).split("/", 4); + if (tokens.length != 4) return string; + return "https://cdn.jsdelivr.net/gh/" + + tokens[0] + "/" + tokens[1] + "@" + tokens[2] + "/" + tokens[3]; + } + return string; + } } diff --git a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java index 2383173..1a56104 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -8,15 +8,22 @@ import com.topjohnwu.superuser.io.SuFileInputStream; import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; +import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Locale; +import java.util.Objects; public class PropUtils { private static final HashMap moduleSupportsFallbacks = new HashMap<>(); private static final HashMap moduleConfigsFallbacks = new HashMap<>(); private static final HashMap moduleMinApiFallbacks = new HashMap<>(); + private static final HashSet moduleImportantProp = new HashSet<>(Arrays.asList( + "id", "name", "version", "versionCode" + )); private static final int RIRU_MIN_API; // Note: These fallback values may not be up-to-date @@ -50,11 +57,23 @@ public class PropUtils { moduleMinApiFallbacks.put("riru-core", RIRU_MIN_API = Build.VERSION_CODES.M); } - public static void readProperties(ModuleInfo moduleInfo, String file,boolean local) throws IOException { + public static void readProperties(ModuleInfo moduleInfo, String file, + boolean local) throws IOException { + readProperties(moduleInfo, SuFileInputStream.open(file), file, local); + } + + public static void readProperties(ModuleInfo moduleInfo, String file, + String name, boolean local) throws IOException { + readProperties(moduleInfo, SuFileInputStream.open(file), name, local); + } + + public static void readProperties(ModuleInfo moduleInfo, InputStream inputStream, + String name, boolean local) throws IOException { boolean readId = false, readIdSec = false, readName = false, - readVersionCode = false, readVersion = false, invalid = false; + readVersionCode = false, readVersion = false, readDescription = false, + readUpdateJson = false, invalid = false; try (BufferedReader bufferedReader = new BufferedReader( - new InputStreamReader(SuFileInputStream.open(file), StandardCharsets.UTF_8))) { + new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { String line; int lineNum = 0; while ((line = bufferedReader.readLine()) != null) { @@ -70,11 +89,15 @@ public class PropUtils { invalid = true; continue; } else throw new IOException("Invalid key at line " + lineNum); - } else if (isInvalidValue(value) && !key.equals("id") && !key.equals("name")) { - if (local) { - invalid = true; - continue; - } else throw new IOException("Invalid value for key " + key); + } else { + if (value.isEmpty() && !moduleImportantProp.contains(key)) + continue; // allow empty values to pass. + if (isInvalidValue(value)) { + if (local) { + invalid = true; + continue; + } else throw new IOException("Invalid value for key " + key); + } } switch (key) { case "id": @@ -89,7 +112,7 @@ public class PropUtils { if (local) { invalid = true; } else { - throw new IOException(file + " has an non matching module id! " + + throw new IOException(name + " has an non matching module id! " + "(Expected \"" + moduleInfo.id + "\" got \"" + value + "\""); } } @@ -134,6 +157,12 @@ public class PropUtils { break; case "description": moduleInfo.description = value; + readDescription = true; + break; + case "updateJson": + if (isInvalidURL(value)) break; + moduleInfo.updateJson = Http.fixUpLink(value); + readUpdateJson = true; break; case "support": // Do not accept invalid or too broad support links @@ -198,13 +227,19 @@ public class PropUtils { throw new IOException("Didn't read module versionCode at least once!"); } } - if (moduleInfo.name == null || !readName) { + if (!readName || isInvalidValue(moduleInfo.name)) { moduleInfo.name = makeNameFromId(moduleInfo.id); } // We can't accept too long version names for usability reason. - if (moduleInfo.version == null || !readVersion || moduleInfo.version.length() > 16) { + if (!readVersion || moduleInfo.version.length() > 16) { moduleInfo.version = "v" + moduleInfo.versionCode; } + if (!readDescription || isInvalidValue(moduleInfo.description)) { + moduleInfo.description = ""; + } + if (!readUpdateJson) { + moduleInfo.updateJson = null; + } if (moduleInfo.minApi == 0) { Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id); if (minApiFallback != null) diff --git a/app/src/main/res/drawable/ic_baseline_access_time_24.xml b/app/src/main/res/drawable/ic_baseline_access_time_24.xml new file mode 100644 index 0000000..86533bf --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_access_time_24.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_baseline_sort_by_alpha_24.xml b/app/src/main/res/drawable/ic_baseline_sort_by_alpha_24.xml new file mode 100644 index 0000000..93e2b9a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_sort_by_alpha_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60aa0b0..53d0a7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,10 +9,17 @@ The application is in lockdown mode Failed to download file. Modules took too long to boot, consider disabling some modules - Fail to connect to the internet + Failed to connect to the internet SettingsActivity Application update available Update + No description found. + Download module + Install module + Update module + Changelog + Support + Donate Last update: @@ -61,4 +68,17 @@ Some modules do not declare their metadata properly,causing visual glitches, and/or indicating poor module quality, disable at your own risk! + Dns over https + May fix connections issues in some cases. + Disable extensions + + Disable Fox\'s Mmm extensions, this prevent modules from using + terminal extensions, useful if a module misuse Fox\'s Mmm extensions. + + Warp text + + Warp text to new line instead of putting all text on the same line + + Repo enabled + Repo disabled \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 16f662f..b0e999a 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -31,7 +31,7 @@ @color/purple_200 @color/purple_700 - @color/black + @color/white @color/teal_200 @color/teal_200 diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 07f0b48..90d0606 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -8,14 +8,15 @@ app:title="@string/theme_pref" app:defaultValue="system" app:entries="@array/theme_values_names" - app:entryValues="@array/theme_values" /> + app:entryValues="@array/theme_values" + app:singleLineTitle="false" /> + app:singleLineTitle="false" /> + app:summary="@string/showcase_mode_desc" + app:singleLineTitle="false" /> + app:title="@string/loading" + app:singleLineTitle="false" /> + app:title="@string/loading" + app:singleLineTitle="false" /> + app:title="@string/app_update" + app:singleLineTitle="false" /> + app:title="@string/source_code" + app:singleLineTitle="false" /> + app:title="@string/show_licenses" + app:singleLineTitle="false" /> \ No newline at end of file diff --git a/build.gradle b/build.gradle index a8851ef..2d0b01f 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { } project.ext.latestAboutLibsRelease = "8.9.4" dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' + classpath 'com.android.tools.build:gradle:7.1.0-rc01' classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:${latestAboutLibsRelease}" // NOTE: Do not place your application dependencies here; they belong