Improve/rework Androidacy compatibility

pull/213/head
Fox2Code 2 years ago
parent fd4e683698
commit efa6c14289

@ -111,5 +111,16 @@
android:name="androidx.work.WorkManagerInitializer"
tools:node="remove" />
</provider>
<provider
android:authorities="${applicationId}.file-provider"
android:name="androidx.core.content.FileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/shared_file_paths" />
</provider>
</application>
</manifest>

@ -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);
}
}
}

@ -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);
}
}

@ -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=");

@ -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(() -> {

@ -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) {

@ -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();

@ -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<Uri> 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();
});
}
}

@ -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;
}
}

@ -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) {

@ -6,11 +6,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
app:fitsSystemWindowsInsets="left|right">
<WebView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/webView" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progress_bar"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:indeterminate="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Share folder under internal cache folder. The base folder is context.getCacheDir() -->
<cache-path name="internal_cache" path="/" />
</paths>
Loading…
Cancel
Save