diff --git a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java index 40cba9f..73ef5ac 100644 --- a/app/src/main/java/com/fox2code/mmm/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/ActionButtonType.java @@ -11,17 +11,23 @@ import androidx.annotation.DrawableRes; import com.fox2code.mmm.compat.CompatActivity; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.manager.ModuleManager; -import com.fox2code.mmm.repo.RepoModule; import com.fox2code.mmm.utils.IntentHelper; public enum ActionButtonType { INFO(R.drawable.ic_baseline_info_24) { @Override public void doAction(ImageButton button, ModuleHolder moduleHolder) { - IntentHelper.openMarkdown(button.getContext(), - moduleHolder.repoModule.notesUrl, - moduleHolder.repoModule.moduleInfo.name, - moduleHolder.getMainModuleConfig()); + String notesUrl = moduleHolder.repoModule.notesUrl; + if (notesUrl.startsWith("https://api.androidacy.com/magisk/readme/?module=") || + notesUrl.startsWith("https://www.androidacy.com/")) { + IntentHelper.openUrlAndroidacy(button.getContext(), notesUrl, false, + moduleHolder.repoModule.moduleInfo.name, + moduleHolder.getMainModuleConfig()); + } else { + IntentHelper.openMarkdown(button.getContext(), notesUrl, + moduleHolder.repoModule.moduleInfo.name, + moduleHolder.getMainModuleConfig()); + } } @Override @@ -47,6 +53,11 @@ public enum ActionButtonType { if (moduleInfo == null) return; String updateZipUrl = moduleHolder.getUpdateZipUrl(); if (updateZipUrl == null) return; + if (updateZipUrl.startsWith("https://www.androidacy.com/")) { + IntentHelper.openUrlAndroidacy( + button.getContext(), updateZipUrl, true); + return; + } String updateZipChecksum = moduleHolder.getUpdateZipChecksum(); IntentHelper.openInstaller(button.getContext(), updateZipUrl, moduleInfo.name, moduleInfo.config, updateZipChecksum); diff --git a/app/src/main/java/com/fox2code/mmm/Constants.java b/app/src/main/java/com/fox2code/mmm/Constants.java index 515cc2a..d585be2 100644 --- a/app/src/main/java/com/fox2code/mmm/Constants.java +++ b/app/src/main/java/com/fox2code/mmm/Constants.java @@ -18,6 +18,8 @@ public class Constants { public static final String EXTRA_INSTALL_NO_EXTENSIONS = "extra_install_no_extensions"; public static final String EXTRA_INSTALL_TEST_ROOTLESS = "extra_install_test_rootless"; public static final String EXTRA_ANDROIDACY_ALLOW_INSTALL = "extra_androidacy_allow_install"; + public static final String EXTRA_ANDROIDACY_ACTIONBAR_TITLE = "extra_androidacy_actionbar_title"; + public static final String EXTRA_ANDROIDACY_ACTIONBAR_CONFIG = "extra_androidacy_actionbar_config"; 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 2836d54..d33a570 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -32,6 +32,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O public LinearProgressIndicator progressIndicator; private ModuleViewAdapter moduleViewAdapter; private SwipeRefreshLayout swipeRefreshLayout; + private long swipeRefreshBlocker = 0; private RecyclerView moduleList; private CardView searchCard; private SearchView searchView; @@ -54,6 +55,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O this.setTitle(R.string.app_name); this.progressIndicator = findViewById(R.id.progress_bar); this.swipeRefreshLayout = findViewById(R.id.swipe_refresh); + this.swipeRefreshBlocker = Long.MAX_VALUE; this.moduleList = findViewById(R.id.module_list); this.searchCard = findViewById(R.id.search_card); this.searchView = findViewById(R.id.search_bar); @@ -106,6 +108,7 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O } public void commonNext() { + swipeRefreshBlocker = System.currentTimeMillis() + 5_000L; moduleViewListBuilder.setFooterPx(searchCard.getHeight()); // Fix an edge case if (MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE); @@ -222,12 +225,15 @@ public class MainActivity extends CompatActivity implements SwipeRefreshLayout.O @Override public void onRefresh() { - if (this.initMode || this.progressIndicator == null || + if (this.swipeRefreshBlocker > System.currentTimeMillis() || + this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE) { + this.swipeRefreshLayout.setRefreshing(false); return; // Do not double scan } this.progressIndicator.setVisibility(View.VISIBLE); this.progressIndicator.setProgressCompat(0, false); + this.swipeRefreshBlocker = System.currentTimeMillis() + 5_000L; // this.swipeRefreshLayout.setRefreshing(true); ?? new Thread(() -> { Http.cleanDnsCache(); // Allow DNS reload from network diff --git a/app/src/main/java/com/fox2code/mmm/ModuleHolder.java b/app/src/main/java/com/fox2code/mmm/ModuleHolder.java index 6c0896a..a164b8d 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleHolder.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleHolder.java @@ -153,7 +153,7 @@ public final class ModuleHolder implements Comparable { if (this.moduleInfo != null && !showcaseMode) { buttonTypeList.add(ActionButtonType.UNINSTALL); } - if (this.repoModule != null) { + if (this.repoModule != null && this.repoModule.notesUrl != null) { buttonTypeList.add(ActionButtonType.INFO); } if ((this.repoModule != null || (this.moduleInfo != null && diff --git a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java index 02716ca..f609f26 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleViewAdapter.java @@ -13,7 +13,6 @@ import android.widget.ImageButton; import android.widget.TextView; import androidx.annotation.ColorInt; -import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.annotation.StringRes; import androidx.cardview.widget.CardView; @@ -22,7 +21,6 @@ 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; import com.google.android.material.switchmaterial.SwitchMaterial; import com.topjohnwu.superuser.internal.UiThreadHandler; diff --git a/app/src/main/java/com/fox2code/mmm/ModuleViewListBuilder.java b/app/src/main/java/com/fox2code/mmm/ModuleViewListBuilder.java index d8b3bdc..4102461 100644 --- a/app/src/main/java/com/fox2code/mmm/ModuleViewListBuilder.java +++ b/app/src/main/java/com/fox2code/mmm/ModuleViewListBuilder.java @@ -207,7 +207,8 @@ public class ModuleViewListBuilder { String query = this.query; String idLw = moduleInfo.id.toLowerCase(Locale.ROOT); String nameLw = moduleInfo.name.toLowerCase(Locale.ROOT); - String authorLw = moduleInfo.author.toLowerCase(Locale.ROOT); + String authorLw = moduleInfo.author == null ? "" : + moduleInfo.author.toLowerCase(Locale.ROOT); if (query.isEmpty() || query.equals(idLw) || query.equals(nameLw) || query.equals(authorLw)) { moduleHolder.filterLevel = 0; // Lower = better diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java index e85aa08..f0eefe7 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -2,9 +2,12 @@ package com.fox2code.mmm.androidacy; import android.annotation.SuppressLint; import android.content.Intent; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebSettings; import android.webkit.WebView; @@ -46,8 +49,26 @@ public class AndroidacyActivity extends CompatActivity { } boolean allowInstall = intent.getBooleanExtra( Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false); + String title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE); + String config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG); this.setContentView(R.layout.webview); - this.hideActionBar(); + if (title == null || title.isEmpty()) { + this.hideActionBar(); + } else { // Only used for note section + this.setTitle(title); + this.setDisplayHomeAsUpEnabled(true); + if (config != null && !config.isEmpty()) { + String configPkg = IntentHelper.getPackageOfConfig(config); + try { + this.getPackageManager().getPackageInfo(configPkg, 0); + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, + menu -> { + IntentHelper.openConfig(this, config); + return true; + }); + } catch (PackageManager.NameNotFoundException ignored) {} + } + } this.webView = this.findViewById(R.id.webView); WebSettings webSettings = this.webView.getSettings(); webSettings.setUserAgentString(Http.getAndroidacyUA()); @@ -69,6 +90,17 @@ public class AndroidacyActivity extends CompatActivity { return false; } }); + this.webView.setWebChromeClient(new WebChromeClient() { + @Override + public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, + FileChooserParams fileChooserParams) { + CompatActivity.getCompatActivity(webView).startActivityForResult( + fileChooserParams.createIntent(), (code, data) -> + filePathCallback.onReceiveValue( + FileChooserParams.parseResult(code, data))); + return true; + } + }); this.webView.addJavascriptInterface( new AndroidacyWebAPI(this, allowInstall), "mmm"); this.webView.loadUrl(uri.toString()); diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java new file mode 100644 index 0000000..3c300dc --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyRepoData.java @@ -0,0 +1,189 @@ +package com.fox2code.mmm.androidacy; + +import android.content.SharedPreferences; +import android.util.Log; +import android.webkit.CookieManager; + +import com.fox2code.mmm.R; +import com.fox2code.mmm.manager.ModuleInfo; +import com.fox2code.mmm.repo.RepoData; +import com.fox2code.mmm.repo.RepoModule; +import com.fox2code.mmm.utils.Http; +import com.fox2code.mmm.utils.PropUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public class AndroidacyRepoData extends RepoData { + private static final String TAG = "AndroidacyRepoData"; + private long androidacyBlockade = 0; + + public AndroidacyRepoData(String url, File cacheRoot, + SharedPreferences cachedPreferences) { + super(url, cacheRoot, cachedPreferences); + } + + @Override + protected boolean prepare() { + // Implementation details discussed on telegram + long time = System.currentTimeMillis(); + if (this.androidacyBlockade > time) return false; + this.androidacyBlockade = time + 5_000L; + String cookies = CookieManager.getInstance().getCookie("https://.androidacy.com/"); + int start = cookies.indexOf("USER="); + String token = null; + if (start != -1) { + int end = cookies.indexOf(";", start); + if (end != -1) + token = cookies.substring(start, end); + } + if (token != null) { + try { + Http.doHttpGet("https://api.androidacy.com/auth/me",true); + } catch (Exception e) { + if ("Received error code: 419".equals(e.getMessage()) || + "Received error code: 429".equals(e.getMessage())) { + Log.e(TAG, "We are being rate limited!", e); + this.androidacyBlockade = time + 3_600_000L; + return false; + } + Log.w(TAG, "Invalid token, resetting..."); + CookieManager.getInstance().setCookie("https://.androidacy.com/", + "USER=; expires=Thu, 01 Jan 1970 00:00:00 GMT;" + + " path=/; secure; domain=.androidacy.com"); + token = null; + } + } + if (token == null) { + try { + Log.i(TAG, "Refreshing token..."); + token = new String(Http.doHttpPost( + "https://api.androidacy.com/auth/register", + "",true), StandardCharsets.UTF_8); + CookieManager.getInstance().setCookie(".androidacy.com", + "USER="+ token + "; expires=Fri, 31 Dec 9999 23:59:59 GMT;" + + " path=/; secure; domain=.androidacy.com"); + } catch (Exception e) { + if ("Received error code: 419".equals(e.getMessage()) || + "Received error code: 429".equals(e.getMessage())) { + Log.e(TAG, "We are being rate limited!", e); + this.androidacyBlockade = time + 3_600_000L; + } + Log.e(TAG, "Failed to get a new token", e); + return false; + } + } + return true; + } + + @Override + protected List populate(JSONObject jsonObject) throws JSONException { + if (!jsonObject.getString("status").equals("success")) + throw new JSONException("Response is not a success!"); + String name = jsonObject.optString( + "name", "Androidacy Modules Repo"); + String nameForModules = name.endsWith(" (Official)") ? + name.substring(0, name.length() - 11) : name; + JSONArray jsonArray = jsonObject.getJSONArray("data"); + for (RepoModule repoModule : this.moduleHashMap.values()) { + repoModule.processed = false; + } + ArrayList newModules = new ArrayList<>(); + int len = jsonArray.length(); + long lastLastUpdate = 0; + for (int i = 0; i < len; i++) { + jsonObject = jsonArray.getJSONObject(i); + String moduleId = jsonObject.getString("codename"); + long lastUpdate = jsonObject.getLong("updated_at") * 1000; + lastLastUpdate = Math.max(lastLastUpdate, lastUpdate); + RepoModule repoModule = this.moduleHashMap.get(moduleId); + if (repoModule == null) { + repoModule = new RepoModule(moduleId); + repoModule.moduleInfo.flags = 0; + this.moduleHashMap.put(moduleId, repoModule); + newModules.add(repoModule); + } else { + if (repoModule.lastUpdated < lastUpdate) { + newModules.add(repoModule); + } + } + repoModule.processed = true; + repoModule.lastUpdated = lastUpdate; + repoModule.repoName = nameForModules; + repoModule.zipUrl = filterURL( + jsonObject.optString("zipUrl", "")); + repoModule.notesUrl = filterURL( + jsonObject.optString("notesUrl", "")); + if (repoModule.zipUrl == null) + repoModule.zipUrl = jsonObject.getString("url"); + if (repoModule.notesUrl == null) { + repoModule.notesUrl = // Fallback url in case the API doesn't return one + "https://api.androidacy.com/magisk/readme/?module=" + moduleId; + } + repoModule.qualityText = R.string.module_downloads; + repoModule.qualityValue = jsonObject.optInt("downloads", 0); + String checksum = jsonObject.optString("checksum", ""); + repoModule.checksum = checksum.isEmpty() ? null : checksum; + ModuleInfo moduleInfo = repoModule.moduleInfo; + moduleInfo.name = jsonObject.getString("name"); + moduleInfo.versionCode = jsonObject.getLong("versionCode"); + moduleInfo.version = jsonObject.optString( + "version", "v" + moduleInfo.versionCode); + moduleInfo.author = jsonObject.optString("author", "Unknown"); + moduleInfo.minApi = jsonObject.getInt("minApi"); + moduleInfo.maxApi = jsonObject.getInt("maxApi"); + String minMagisk = jsonObject.getString("minMagisk"); + try { + int c = minMagisk.indexOf('.'); + if (c == -1) { + moduleInfo.minMagisk = Integer.parseInt(minMagisk); + } else { + moduleInfo.minMagisk = // Allow 24.1 to mean 24100 + (Integer.parseInt(minMagisk.substring(0, c)) * 1000) + + (Integer.parseInt(minMagisk.substring(c + 1)) * 100); + } + } catch (Exception e) { + moduleInfo.minMagisk = 0; + } + moduleInfo.support = filterURL(jsonObject.optString("support")); + moduleInfo.donate = filterURL(jsonObject.optString("donate")); + String config = jsonObject.optString("config", ""); + moduleInfo.config = config.isEmpty() ? null : config; + PropUtils.applyFallbacks(moduleInfo); // Apply fallbacks + Log.d(TAG, "Module " + moduleInfo.name + " " + moduleInfo.id + " " + + moduleInfo.version + " " + moduleInfo.versionCode); + } + Iterator moduleInfoIterator = this.moduleHashMap.values().iterator(); + while (moduleInfoIterator.hasNext()) { + RepoModule repoModule = moduleInfoIterator.next(); + if (!repoModule.processed) { + moduleInfoIterator.remove(); + } + } + this.lastUpdate = lastLastUpdate; + this.name = name; + return newModules; + } + + private static String filterURL(String url) { + if (url == null || url.isEmpty() || PropUtils.isInvalidURL(url)) { + return null; + } + return url; + } + + @Override + public void storeMetadata(RepoModule repoModule, byte[] data) {} + + @Override + public boolean tryLoadMetadata(RepoModule repoModule) { + return true; + } +} diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java index 1e3b7d5..adb96b1 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java @@ -122,4 +122,12 @@ public class AndroidacyWebAPI { LocalModuleInfo localModuleInfo = ModuleManager.getINSTANCE().getModules().get(moduleId); return localModuleInfo != null ? localModuleInfo.updateVersionCode : -1L; } + + /** + * Hide action bar if visible, the action bar is only visible by default on notes. + */ + @JavascriptInterface + public void hideActionBar() { + this.activity.hideActionBar(); + } } 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 a1fea85..3eb723c 100644 --- a/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java +++ b/app/src/main/java/com/fox2code/mmm/manager/ModuleManager.java @@ -4,17 +4,13 @@ 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; diff --git a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java index 0d0fede..e71e21b 100644 --- a/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java +++ b/app/src/main/java/com/fox2code/mmm/markdown/MarkdownActivity.java @@ -28,9 +28,9 @@ public class MarkdownActivity extends CompatActivity { super.onCreate(savedInstanceState); this.setDisplayHomeAsUpEnabled(true); Intent intent = this.getIntent(); - if (intent == null || !MainApplication.checkSecret(intent)) { + if (!MainApplication.checkSecret(intent)) { Log.e(TAG, "Impersonation detected!"); - this.onBackPressed(); + this.forceBackPressed(); return; } String url = intent.getExtras() 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 dc43cf2..7c76a7c 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoData.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoData.java @@ -19,7 +19,6 @@ 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; @@ -40,7 +39,7 @@ public class RepoData { private final Map specialTimes; private long specialLastUpdate; - RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) { + protected RepoData(String url, File cacheRoot, SharedPreferences cachedPreferences) { this(url, cacheRoot, cachedPreferences, false); } @@ -95,7 +94,11 @@ public class RepoData { } } - List populate(JSONObject jsonObject) throws JSONException { + protected boolean prepare() { + return true; + } + + protected List populate(JSONObject jsonObject) throws JSONException { List newModules = new ArrayList<>(); synchronized (this.populateLock) { String name = jsonObject.getString("name").trim(); 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 fedc153..5b56884 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.util.Log; import com.fox2code.mmm.MainApplication; +import com.fox2code.mmm.androidacy.AndroidacyRepoData; import com.fox2code.mmm.manager.ModuleInfo; import com.fox2code.mmm.utils.Files; import com.fox2code.mmm.utils.Hashes; @@ -29,6 +30,8 @@ public final class RepoManager { "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 ANDROIDACY_MAGISK_REPO_ENDPOINT = + "https://api.androidacy.com/magisk/repo"; public static final String MAGISK_REPO_HOMEPAGE = "https://github.com/Magisk-Modules-Repo"; @@ -67,6 +70,7 @@ public final class RepoManager { // We do not have repo list config yet. this.addRepoData(MAGISK_REPO_JSDELIVR); this.addRepoData(MAGISK_ALT_REPO_JSDELIVR); + this.addAndroidacyRepoData(); // Populate default cache for (RepoData repoData:this.repoData.values()) { for (RepoModule repoModule:repoData.moduleHashMap.values()) { @@ -79,7 +83,8 @@ public final class RepoManager { this.modules.put(repoModule.id, repoModule); } } else { - Log.e(TAG, "Detected module with invalid metadata: " + repoModule.id); + Log.e(TAG, "Detected module with invalid metadata: " + + repoModule.repoName + "/" + repoModule.id); } } } @@ -103,7 +108,11 @@ public final class RepoManager { synchronized (this.repoUpdateLock) { repoData = this.repoData.get(url); if (repoData == null) { - return this.addRepoData(url); + if (ANDROIDACY_MAGISK_REPO_ENDPOINT.equals(url)) { + return this.addAndroidacyRepoData(); + } else { + return this.addRepoData(url); + } } } return repoData; @@ -170,13 +179,16 @@ public final class RepoManager { for (int i = 0; i < repoUpdaters.length; i++) { List repoModules = repoUpdaters[i].toUpdate(); RepoData repoData = repoDatas[i]; + Log.d(TAG, "Registering " + repoData.name); 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 || + if (repoModule.propUrl != null) { + repoData.storeMetadata(repoModule, + Http.doHttpGet(repoModule.propUrl, false)); + Files.write(new File(repoData.cacheRoot, repoModule.id + ".prop"), + Http.doHttpGet(repoModule.propUrl, false)); + } + if (repoData.tryLoadMetadata(repoModule) && (allowLowQualityModules || !PropUtils.isLowQualityModule(repoModule.moduleInfo))) { // Note: registeredRepoModule may not be null if registered by multiple repos RepoModule registeredRepoModule = this.modules.get(repoModule.id); @@ -233,6 +245,8 @@ public final class RepoManager { case MAGISK_ALT_REPO: case MAGISK_ALT_REPO_JSDELIVR: return "magisk_alt_repo"; + case ANDROIDACY_MAGISK_REPO_ENDPOINT: + return "androidacy_repo"; default: return "repo_" + Hashes.hashSha1( url.getBytes(StandardCharsets.UTF_8)); @@ -249,4 +263,14 @@ public final class RepoManager { this.repoData.put(url, repoData); return repoData; } + + private RepoData addAndroidacyRepoData() { + File cacheRoot = new File(this.mainApplication.getCacheDir(), "androidacy_repo"); + SharedPreferences sharedPreferences = this.mainApplication + .getSharedPreferences("mmm_androidacy_repo", Context.MODE_PRIVATE); + RepoData repoData = new AndroidacyRepoData( + ANDROIDACY_MAGISK_REPO_ENDPOINT, cacheRoot, sharedPreferences); + this.repoData.put(ANDROIDACY_MAGISK_REPO_ENDPOINT, repoData); + return repoData; + } } diff --git a/app/src/main/java/com/fox2code/mmm/repo/RepoModule.java b/app/src/main/java/com/fox2code/mmm/repo/RepoModule.java index 2bcab7b..a374da6 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoModule.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoModule.java @@ -1,5 +1,7 @@ package com.fox2code.mmm.repo; +import androidx.annotation.StringRes; + import com.fox2code.mmm.manager.ModuleInfo; public class RepoModule { @@ -11,7 +13,10 @@ public class RepoModule { public String zipUrl; public String notesUrl; public String checksum; - boolean processed; + public boolean processed; + @StringRes + public int qualityText; + public int qualityValue; public RepoModule(String id) { this.moduleInfo = new ModuleInfo(id); 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 a8e133a..492b48f 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoUpdater.java @@ -27,6 +27,9 @@ public class RepoUpdater { public int fetchIndex() { try { + if (!this.repoData.prepare()) { + return 0; + } this.indexRaw = Http.doHttpGet(this.repoData.url, false); if (this.repoData.special) this.repoData.updateSpecialTimes(true); this.toUpdate = this.repoData.populate(new JSONObject( 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 dbc0da2..574e9a7 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -160,22 +160,25 @@ public class SettingsActivity extends CompatActivity { setPreferencesFromResource(R.xml.repo_preferences, rootKey); setRepoData("pref_repo_main", RepoManager.MAGISK_REPO, "Magisk Modules Repo (Official)", RepoManager.MAGISK_REPO_HOMEPAGE, - null, null); + null, null,null); setRepoData("pref_repo_alt", RepoManager.MAGISK_ALT_REPO, "Magisk Modules Alt Repo", RepoManager.MAGISK_ALT_REPO_HOMEPAGE, - null, + null, null, "https://github.com/Magisk-Modules-Alt-Repo/submission/issues"); // Androidacy backend not yet implemented! - setRepoData("pref_repo_androidacy", null, - "Androidacy Magisk Modules Repo", + setRepoData("pref_repo_androidacy", + RepoManager.ANDROIDACY_MAGISK_REPO_ENDPOINT, + "Androidacy Modules Repo", RepoManager.ANDROIDACY_MAGISK_REPO_HOMEPAGE, "https://t.me/androidacy_discussions", + "https://patreon.com/androidacy", "https://www.androidacy.com/module-repository-applications/"); } private void setRepoData(String preferenceName, String url, String fallbackTitle, String homepage, - String supportUrl, String submissionUrl) { + String supportUrl, String donateUrl, + String submissionUrl) { Preference preference = findPreference(preferenceName); if (preference == null) return; RepoData repoData = RepoManager.getINSTANCE().get(url); @@ -200,6 +203,13 @@ public class SettingsActivity extends CompatActivity { return true; }); } + preference = findPreference(preferenceName + "_donate"); + if (preference != null && donateUrl != null) { + preference.setOnPreferenceClickListener(p -> { + IntentHelper.openUrl(getCompatActivity(this), donateUrl); + return true; + }); + } preference = findPreference(preferenceName + "_submit"); if (preference != null && submissionUrl != null) { preference.setOnPreferenceClickListener(p -> { 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 4867cfb..1489b36 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -10,6 +10,7 @@ import android.util.Log; import android.webkit.CookieManager; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.MainApplication; @@ -22,6 +23,7 @@ import java.io.InputStream; import java.net.InetAddress; import java.net.Proxy; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,7 +31,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.concurrent.TimeUnit; @@ -38,12 +39,15 @@ import okhttp3.Cookie; import okhttp3.CookieJar; import okhttp3.Dns; import okhttp3.HttpUrl; +import okhttp3.MediaType; import okhttp3.OkHttpClient; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.ResponseBody; import okhttp3.brotli.BrotliInterceptor; import okhttp3.dnsoverhttps.DnsOverHttps; +import okio.BufferedSink; public class Http { private static final String TAG = "Http"; @@ -152,8 +156,29 @@ public class Http { Response response = (allowCache ? httpClientWithCache : httpClient).newCall( makeRequestBuilder().url(url).get().build() ).execute(); - // 200 == success, 304 == cache valid - if (response.code() != 200 && (response.code() != 304 || !allowCache)) { + // 200/204 == success, 304 == cache valid + if (response.code() != 200 && response.code() != 204 && + (response.code() != 304 || !allowCache)) { + throw new IOException("Received error code: "+ response.code()); + } + ResponseBody responseBody = response.body(); + // Use cache api if used cached response + if (responseBody == null && response.code() == 304) { + response = response.cacheResponse(); + if (response != null) + responseBody = response.body(); + } + return responseBody == null ? new byte[0] : responseBody.bytes(); + } + + public static byte[] doHttpPost(String url,String data,boolean allowCache) throws IOException { + Response response = (allowCache ? httpClientWithCache : httpClient).newCall( + makeRequestBuilder().url(url).post(JsonRequestBody.from(data)) + .header("Content-Type", "application/json").build() + ).execute(); + // 200/204 == success, 304 == cache valid + if (response.code() != 200 && response.code() != 204 && + (response.code() != 304 || !allowCache)) { throw new IOException("Received error code: "+ response.code()); } ResponseBody responseBody = response.body(); @@ -169,7 +194,7 @@ public class Http { public static byte[] doHttpGet(String url,ProgressListener progressListener) throws IOException { Log.d("Http", "Progress URL: " + url); Response response = httpClient.newCall(makeRequestBuilder().url(url).get().build()).execute(); - if (response.code() != 200) { + if (response.code() != 200 && response.code() != 204) { throw new IOException("Received error code: "+ response.code()); } ResponseBody responseBody = Objects.requireNonNull(response.body()); @@ -378,6 +403,40 @@ public class Http { } } + private static class JsonRequestBody extends RequestBody { + private static final MediaType JSON_MEDIA_TYPE = MediaType.get("application/json"); + private static final JsonRequestBody EMPTY = new JsonRequestBody(new byte[0]); + + static JsonRequestBody from(String data) { + if (data == null || data.length() == 0) { + return EMPTY; + } + return new JsonRequestBody(data.getBytes(StandardCharsets.UTF_8)); + } + + final byte[] data; + + private JsonRequestBody(byte[] data) { + this.data = data; + } + + @Nullable + @Override + public MediaType contentType() { + return JSON_MEDIA_TYPE; + } + + @Override + public long contentLength() { + return this.data.length; + } + + @Override + public void writeTo(@NonNull BufferedSink bufferedSink) throws IOException { + bufferedSink.write(this.data); + } + } + /** * Change URL to appropriate url and force Magisk link to use latest version. */ diff --git a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java index f7d7c8d..730f27e 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -27,9 +27,7 @@ import com.fox2code.mmm.markdown.MarkdownActivity; import com.topjohnwu.superuser.io.SuFileInputStream; import java.io.File; -import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -46,12 +44,21 @@ public class IntentHelper { } public static void openUrlAndroidacy(Context context, String url,boolean allowInstall) { + openUrlAndroidacy(context, url, allowInstall, null, null); + } + + public static void openUrlAndroidacy(Context context, String url, boolean allowInstall, + String title,String config) { Uri uri = Uri.parse(url); try { Intent myIntent = new Intent( Constants.INTENT_ANDROIDACY_INTERNAL, uri, context, AndroidacyActivity.class); myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, allowInstall); + if (title != null) + myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE, title); + if (config != null) + myIntent.putExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG, config); MainApplication.addSecret(myIntent); context.startActivity(myIntent); } catch (ActivityNotFoundException e) { 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 fc5fc63..af70e03 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java +++ b/app/src/main/java/com/fox2code/mmm/utils/PropUtils.java @@ -15,7 +15,6 @@ 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<>(); @@ -275,6 +274,23 @@ public class PropUtils { } } + public static void applyFallbacks(ModuleInfo moduleInfo) { + if (moduleInfo.support == null || moduleInfo.support.isEmpty()) { + moduleInfo.support = moduleSupportsFallbacks.get(moduleInfo.id); + } + if (moduleInfo.config == null || moduleInfo.config.isEmpty()) { + moduleInfo.config = moduleConfigsFallbacks.get(moduleInfo.id); + } + if (moduleInfo.minApi == 0) { + Integer minApiFallback = moduleMinApiFallbacks.get(moduleInfo.id); + if (minApiFallback != null) + moduleInfo.minApi = minApiFallback; + else if (moduleInfo.id.startsWith("riru_") + || moduleInfo.id.startsWith("riru-")) + moduleInfo.minApi = RIRU_MIN_API; + } + } + // Some module are really so low quality that it has become very annoying. public static boolean isLowQualityModule(ModuleInfo moduleInfo) { final String description; @@ -290,7 +306,7 @@ public class PropUtils { return !TextUtils.isGraphic(name) || name.indexOf('\0') != -1; } - private static boolean isInvalidURL(String url) { + public static boolean isInvalidURL(String url) { int i = url.indexOf('/', 8); int e = url.indexOf('.', 8); return i == -1 || e == -1 || e >= i || !url.startsWith("https://") diff --git a/app/src/main/res/xml/repo_preferences.xml b/app/src/main/res/xml/repo_preferences.xml index afc25d6..03f0d25 100644 --- a/app/src/main/res/xml/repo_preferences.xml +++ b/app/src/main/res/xml/repo_preferences.xml @@ -36,6 +36,11 @@ app:icon="@drawable/ic_baseline_telegram_24" app:title="@string/support" app:singleLineTitle="false" /> +