diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9eae912..888744f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -111,5 +111,16 @@ android:name="androidx.work.WorkManagerInitializer" tools:node="remove" /> + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/MainActivity.java b/app/src/main/java/com/fox2code/mmm/MainActivity.java index 13a32dc..4d17fb2 100644 --- a/app/src/main/java/com/fox2code/mmm/MainActivity.java +++ b/app/src/main/java/com/fox2code/mmm/MainActivity.java @@ -1,5 +1,7 @@ package com.fox2code.mmm; +import android.Manifest; +import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; @@ -16,6 +18,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; import androidx.cardview.widget.CardView; +import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; @@ -36,6 +39,7 @@ import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; import com.fox2code.mmm.utils.NoodleDebug; import com.google.android.material.progressindicator.LinearProgressIndicator; +import com.topjohnwu.superuser.Shell; import eightbitlab.com.blurview.BlurView; import eightbitlab.com.blurview.RenderScriptBlur; @@ -159,6 +163,9 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); noodleDebug.setEnabled(noodleDebugState); noodleDebug.bind(); + noodleDebug.push("Ensure Permissions"); + ensurePermissions(); + noodleDebug.pop(); ModuleManager.getINSTANCE().scan(); ModuleManager.getINSTANCE().runAfterScan( moduleViewListBuilder::appendInstalledModules); @@ -516,4 +523,15 @@ public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRe public int getOverScrollInsetBottom() { return this.overScrollInsetBottom; } + + private void ensurePermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + ContextCompat.checkSelfPermission(this, + Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED) { + // TODO Use standard Android API to ask for permissions + Shell.cmd("pm grant " + this.getPackageName() + " " + + Manifest.permission.POST_NOTIFICATIONS); + } + } } \ No newline at end of file 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 9ebd164..e3bf669 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java @@ -21,6 +21,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebSettingsCompat; import androidx.webkit.WebViewClientCompat; @@ -34,11 +35,14 @@ import com.fox2code.mmm.R; import com.fox2code.mmm.XHooks; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; +import com.google.android.material.progressindicator.LinearProgressIndicator; import org.json.JSONException; import org.json.JSONObject; import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; @@ -54,15 +58,19 @@ public final class AndroidacyActivity extends FoxActivity { } } + File moduleFile; WebView webView; TextView webViewNote; AndroidacyWebAPI androidacyWebAPI; + LinearProgressIndicator progressIndicator; boolean backOnResume; + boolean downloadMode; @SuppressWarnings("deprecation") @Override @SuppressLint({"SetJavaScriptEnabled", "JavascriptInterface", "RestrictedApi"}) protected void onCreate(@Nullable Bundle savedInstanceState) { + this.moduleFile = new File(this.getCacheDir(), "module.zip"); super.onCreate(savedInstanceState); Intent intent = this.getIntent(); Uri uri; @@ -119,6 +127,8 @@ public final class AndroidacyActivity extends FoxActivity { } } } + this.progressIndicator = this.findViewById(R.id.progress_bar); + this.progressIndicator.setMax(100); this.webView = this.findViewById(R.id.webView); this.webViewNote = this.findViewById(R.id.webViewNote); WebSettings webSettings = this.webView.getSettings(); @@ -156,6 +166,7 @@ public final class AndroidacyActivity extends FoxActivity { // Don't open non Androidacy urls inside WebView if (request.isForMainFrame() && !AndroidacyUtil.isAndroidacyLink(request.getUrl())) { + if (downloadMode || backOnResume) return true; Log.i(TAG, "Exiting WebView " + // hideToken in case isAndroidacyLink fail. AndroidacyUtil.hideToken(request.getUrl().toString())); IntentHelper.openUri(view.getContext(), request.getUrl().toString()); @@ -185,6 +196,8 @@ public final class AndroidacyActivity extends FoxActivity { @Override public void onPageFinished(WebView view, String url) { webViewNote.setVisibility(View.GONE); + progressIndicator.setVisibility(View.INVISIBLE); + progressIndicator.setProgressCompat(0, false); } private void onReceivedError(String url, int errorCode) { @@ -247,9 +260,22 @@ public final class AndroidacyActivity extends FoxActivity { } return super.onConsoleMessage(consoleMessage); } + + @Override + public void onProgressChanged(WebView view, int newProgress) { + if (downloadMode) return; + if (newProgress != 100 && // Show progress bar + progressIndicator.getVisibility() != View.VISIBLE) + progressIndicator.setVisibility(View.VISIBLE); + progressIndicator.setProgressCompat(newProgress, true); + if (newProgress == 100 && // Hide progress bar + progressIndicator.getVisibility() != View.INVISIBLE) + progressIndicator.setVisibility(View.INVISIBLE); + } }); this.webView.setDownloadListener(( downloadUrl, userAgent, contentDisposition, mimetype, contentLength) -> { + if (this.downloadMode || this.isDownloadUrl(downloadUrl)) return; if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !this.backOnResume) { AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; if (androidacyWebAPI != null) { @@ -259,7 +285,7 @@ public final class AndroidacyActivity extends FoxActivity { return; // Workaround Androidacy bug final String moduleId = moduleIdOfUrl(downloadUrl); - if (moduleId != null) { + if (moduleId != null && !this.isFileUrl(downloadUrl)) { webView.evaluateJavascript("document.querySelector(" + "\"#download-form input[name=_token]\").value", result -> new Thread("Androidacy popup workaround thread") { @@ -354,10 +380,22 @@ public final class AndroidacyActivity extends FoxActivity { if (i == -1) i = url.length(); if (url.startsWith(prefix)) return url.substring(prefix.length(), i); } + if (this.isFileUrl(url)) { + int i = url.indexOf("&module="); + if (i != -1) { + int j = url.indexOf('&', i + 1); + if (j == -1) { + return url.substring(i + 8); + } else { + return url.substring(i + 8, j); + } + } + } return null; } private boolean isFileUrl(String url) { + if (url == null) return false; for (String prefix : new String[]{ "https://production-api.androidacy.com/magisk/file/", "https://staging-api.androidacy.com/magisk/file/" @@ -367,18 +405,57 @@ public final class AndroidacyActivity extends FoxActivity { return false; } + private boolean isDownloadUrl(String url) { + for (String prefix : new String[]{ + "https://production-api.androidacy.com/magisk/download/", + "https://staging-api.androidacy.com/magisk/download/" + }) { // Make both staging and non staging act the same + if (url.startsWith(prefix)) return true; + } + return false; + } + private boolean megaIntercept(String pageUrl, String fileUrl) { if (pageUrl == null || fileUrl == null) return false; if (this.isFileUrl(fileUrl)) { Log.d(TAG, "megaIntercept(" + AndroidacyUtil.hideToken(pageUrl) + ", " + AndroidacyUtil.hideToken(fileUrl) + ")"); - } + } else return false; final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; - final String moduleId = this.moduleIdOfUrl(pageUrl); - if (moduleId == null || !this.isFileUrl(fileUrl)) return false; + String moduleId = this.moduleIdOfUrl(fileUrl); + if (moduleId == null) moduleId = this.moduleIdOfUrl(pageUrl); + if (moduleId == null) { + Log.d(TAG, "No module id?"); + return false; + } androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, "", androidacyWebAPI.canInstall()); return true; } + + Uri downloadFileAsync(String url) throws IOException { + this.downloadMode = true; + this.runOnUiThread(() -> { + progressIndicator.setIndeterminate(false); + progressIndicator.setVisibility(View.VISIBLE); + }); + byte[] module; + try { + module = Http.doHttpGet(url, (downloaded, total, done) -> + progressIndicator.setProgressCompat((downloaded * 100) / total, true)); + try (FileOutputStream fileOutputStream = new FileOutputStream(this.moduleFile)) { + fileOutputStream.write(module); + } + } finally { + module = null; + this.runOnUiThread(() -> + progressIndicator.setVisibility(View.INVISIBLE)); + } + this.backOnResume = true; + this.downloadMode = false; + return FileProvider.getUriForFile(this, + this.getPackageName() + ".file-provider", + this.moduleFile); + } } diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java index a96228f..7c25bdf 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyUtil.java @@ -24,6 +24,17 @@ public class AndroidacyUtil { uri.getHost().endsWith(".androidacy.com"); } + public static boolean isAndroidacyFileUrl(@Nullable String url) { + if (url == null) return false; + for (String prefix : new String[]{ + "https://production-api.androidacy.com/magisk/file/", + "https://staging-api.androidacy.com/magisk/file/" + }) { // Make both staging and non staging act the same + if (url.startsWith(prefix)) return true; + } + return false; + } + // Avoid logging token public static String hideToken(@NonNull String url) { int i = url.lastIndexOf("token="); 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 2d320c3..d72f581 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java @@ -64,6 +64,7 @@ public class AndroidacyWebAPI { void openNativeModuleDialogRaw(String moduleUrl, String installTitle, String checksum, boolean canInstall) { + Log.d(TAG, "ModuleDialog, downloadUrl: " + AndroidacyUtil.hideToken(moduleUrl)); this.downloadMode = false; RepoModule repoModule = AndroidacyRepoData .getInstance().moduleHashMap.get(installTitle); @@ -92,7 +93,7 @@ public class AndroidacyWebAPI { .setIcon(R.drawable.ic_baseline_extension_24); builder.setNegativeButton(R.string.download_module, (x, y) -> { this.downloadMode = true; - this.activity.webView.loadUrl(moduleUrl); + IntentHelper.openCustomTab(this.activity, moduleUrl); }); if (canInstall) { boolean hasUpdate = false; @@ -115,8 +116,18 @@ public class AndroidacyWebAPI { if (!this.activity.backOnResume) this.consumedAction = false; }); - ExternalHelper.INSTANCE.injectButton(builder, - Uri.parse(moduleUrl), "androidacy_repo"); + ExternalHelper.INSTANCE.injectButton(builder, () -> { + this.downloadMode = true; + try { + return this.activity.downloadFileAsync(moduleUrl); + } catch (IOException e) { + Log.e(TAG, "Failed to download module", e); + AndroidacyWebAPI.this.activity.runOnUiThread(() -> + Toast.makeText(AndroidacyWebAPI.this.activity, + R.string.failed_download, Toast.LENGTH_SHORT).show()); + return null; + } + }, "androidacy_repo"); final int dim5dp = FoxDisplay.dpToPixel(5); builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp); this.activity.runOnUiThread(() -> { 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 278dbe8..a70a99e 100644 --- a/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java +++ b/app/src/main/java/com/fox2code/mmm/installer/InstallerActivity.java @@ -23,6 +23,7 @@ import com.fox2code.mmm.Constants; import com.fox2code.mmm.MainApplication; import com.fox2code.mmm.R; import com.fox2code.mmm.XHooks; +import com.fox2code.mmm.androidacy.AndroidacyUtil; import com.fox2code.mmm.module.ActionButtonType; import com.fox2code.mmm.sentry.SentryBreadcrumb; import com.fox2code.mmm.sentry.SentryMain; @@ -156,6 +157,7 @@ public class InstallerActivity extends FoxActivity { Log.e(TAG, "Failed to delete module cache"); String errMessage = "Failed to download module zip"; byte[] rawModule; + boolean androidacyBlame = false; // In case Androidacy mess-up again... try { Log.i(TAG, (urlMode ? "Downloading: " : "Loading: ") + target); rawModule = urlMode ? Http.doHttpGet(target, (progress, max, done) -> { @@ -172,6 +174,7 @@ public class InstallerActivity extends FoxActivity { this.progressIndicator.setIndeterminate(true); }); if (this.canceled) return; + androidacyBlame = urlMode && AndroidacyUtil.isAndroidacyFileUrl(target); if (checksum != null && !checksum.isEmpty()) { Log.d(TAG, "Checking for checksum: " + checksum); this.runOnUiThread(() -> this.installerTerminal.addLine("- Checking file integrity")); @@ -208,10 +211,15 @@ public class InstallerActivity extends FoxActivity { } } if (!isModule && !isAnyKernel3) { + if (androidacyBlame) { + this.installerTerminal.addLine( + "! Note: The following error is probably an Androidacy backend error"); + } this.setInstallStateFinished(false, "! File is not a valid Magisk module or AnyKernel3 zip", ""); return; } + androidacyBlame = false; if (noPatch) { if (urlMode) { errMessage = "Failed to save module zip"; @@ -237,6 +245,10 @@ public class InstallerActivity extends FoxActivity { this.doInstall(moduleCache, noExtensions, rootless); } catch (IOException e) { Log.e(TAG, errMessage, e); + if (androidacyBlame) { + this.installerTerminal.addLine( + "! Note: The following error is probably an Androidacy backend error"); + } this.setInstallStateFinished(false, "! " + errMessage, ""); } catch (OutOfMemoryError e) { diff --git a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java index c6416c5..58376fc 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java @@ -113,8 +113,9 @@ public enum ActionButtonType { builder.setMessage(desc); } Log.d("Test", "URL: " + updateZipUrl); - builder.setNegativeButton(R.string.download_module, (x, y) -> - IntentHelper.openCustomTab(button.getContext(), updateZipUrl)); + builder.setNegativeButton(R.string.download_module, (x, y) -> { + IntentHelper.openCustomTab(button.getContext(), updateZipUrl); + }); if (hasRoot) { builder.setPositiveButton(moduleHolder.hasUpdate() ? R.string.update_module : R.string.install_module, (x, y) -> { @@ -125,7 +126,7 @@ public enum ActionButtonType { }); } ExternalHelper.INSTANCE.injectButton(builder, - Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo()); + () -> Uri.parse(updateZipUrl), moduleHolder.getUpdateZipRepo()); int dim5dp = FoxDisplay.dpToPixel(5); builder.setBackgroundInsetStart(dim5dp).setBackgroundInsetEnd(dim5dp); AlertDialog alertDialog = builder.show(); diff --git a/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.java b/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.java index 5844e9a..75be7ff 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.java @@ -14,10 +14,10 @@ import android.widget.Toast; import androidx.appcompat.app.AlertDialog; import androidx.core.app.ActivityOptionsCompat; +import androidx.core.util.Supplier; -import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.Constants; -import com.fox2code.mmm.MainApplication; +import com.topjohnwu.superuser.internal.UiThreadHandler; import java.util.List; @@ -60,6 +60,7 @@ public final class ExternalHelper { Bundle param = ActivityOptionsCompat.makeCustomAnimation(context, android.R.anim.fade_in, android.R.anim.fade_out).toBundle(); Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri); + intent.setFlags(IntentHelper.FLAG_GRANT_URI_PERMISSION); intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId); if (multi) { intent = Intent.createChooser(intent, label); @@ -93,15 +94,24 @@ public final class ExternalHelper { return false; } - public void injectButton(AlertDialog.Builder builder, Uri uri, String repoId) { + public void injectButton(AlertDialog.Builder builder, Supplier uriSupplier, String repoId) { if (label == null) return; builder.setNeutralButton(label, (dialog, button) -> { Context context = ((Dialog) dialog).getContext(); - if (!openExternal(context, uri, repoId)) { - Toast.makeText(context, - "Failed to launch external activity", - Toast.LENGTH_SHORT).show(); - } + new Thread("Async downloader") { + @Override + public void run() { + final Uri uri = uriSupplier.get(); + if (uri == null) return; + UiThreadHandler.run(() -> { + if (!openExternal(context, uri, repoId)) { + Toast.makeText(context, + "Failed to launch external activity", + Toast.LENGTH_SHORT).show(); + } + }); + } + }.start(); }); } } 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 5af6ddd..acc2976 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/Http.java +++ b/app/src/main/java/com/fox2code/mmm/utils/Http.java @@ -61,7 +61,7 @@ public class Http { private static final OkHttpClient httpClientNoRedirect; private static final OkHttpClient httpClientNoRedirectDoH; private static final FallBackDNS fallbackDNS; - private static final CookieJar cookieJar; + private static final CDNCookieJar cookieJar; private static final String androidacyUA; private static final boolean hasWebView; private static boolean doh; @@ -311,11 +311,20 @@ public class Http { return androidacyUA; } + public static String getMagiskUA() { + return "Magisk/" + InstallerInitializer.peekMagiskVersion(); + } + public static void setDoh(boolean doh) { Log.d(TAG, "DoH: " + Http.doh + " -> " + doh); Http.doh = doh; } + public static String getAndroidacyCookies(String url) { + if (!AndroidacyUtil.isAndroidacyLink(url)) return ""; + return cookieJar.getAndroidacyCookies(url); + } + /** * Cookie jar that allow CDN cookies, reset on app relaunch * Note: An argument can be made that it allow tracking but @@ -402,6 +411,18 @@ public class Http { cookieMap.put(host, cdnCookie); } } + + String getAndroidacyCookies(String url) { + if (this.cookieManager != null) { + return this.cookieManager.getCookie(url); + } + StringBuilder stringBuilder = new StringBuilder(); + for (Cookie cookie : this.androidacyCookies) { + stringBuilder.append(cookie.toString()).append("; "); + } + stringBuilder.setLength(stringBuilder.length() - 2); + return stringBuilder.toString(); + } } public interface ProgressListener { @@ -576,8 +597,4 @@ public class Http { } return string; } - - public static CookieJar getCookieJar() { - return cookieJar; - } } 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 3ae6844..38ced15 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.java @@ -9,6 +9,7 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.Environment; import android.util.Log; @@ -49,6 +50,11 @@ public class IntentHelper { "android.support.customtabs.extra.TOOLBAR_COLOR"; private static final String EXTRA_TAB_EXIT_ANIMATION_BUNDLE = "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE"; + static final int FLAG_GRANT_URI_PERMISSION = + Build.VERSION.SDK_INT <= Build.VERSION_CODES.LOLLIPOP ? + Intent.FLAG_GRANT_READ_URI_PERMISSION | + Intent.FLAG_GRANT_WRITE_URI_PERMISSION : + Intent.FLAG_GRANT_READ_URI_PERMISSION; public static void openUri(Context context, String uri) { if (uri.startsWith("intent://")) { @@ -67,7 +73,7 @@ public class IntentHelper { public static void openUrl(Context context, String url, boolean forceBrowser) { try { Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - myIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + myIntent.setFlags(FLAG_GRANT_URI_PERMISSION); if (forceBrowser) { myIntent.addCategory(Intent.CATEGORY_BROWSABLE); } @@ -82,8 +88,9 @@ public class IntentHelper { public static void openCustomTab(Context context, String url) { try { Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - viewIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + viewIntent.setFlags(FLAG_GRANT_URI_PERMISSION); Intent tabIntent = new Intent(viewIntent); + tabIntent.setFlags(FLAG_GRANT_URI_PERMISSION); tabIntent.addCategory(Intent.CATEGORY_BROWSABLE); startActivityEx(context, tabIntent, viewIntent); } catch (ActivityNotFoundException e) { diff --git a/app/src/main/res/layout/webview.xml b/app/src/main/res/layout/webview.xml index df26e5f..40ce75c 100644 --- a/app/src/main/res/layout/webview.xml +++ b/app/src/main/res/layout/webview.xml @@ -6,11 +6,21 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:fitsSystemWindowsInsets="left|right"> + + + + + + + \ No newline at end of file