package com.fox2code.mmm.androidacy; import android.annotation.SuppressLint; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.util.Log; import android.view.View; import android.webkit.ConsoleMessage; import android.webkit.ValueCallback; import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebResourceResponse; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.TextView; 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; import androidx.webkit.WebViewFeature; import com.fox2code.foxcompat.FoxActivity; import com.fox2code.mmm.BuildConfig; import com.fox2code.mmm.Constants; import com.fox2code.mmm.MainApplication; 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 java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.util.HashMap; /** * Per Androidacy repo implementation agreement, no request of this WebView shall be modified. */ public final class AndroidacyActivity extends FoxActivity { private static final String TAG = "AndroidacyActivity"; static { if (BuildConfig.DEBUG) { WebView.setWebContentsDebuggingEnabled(true); } } 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; if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) { Log.w(TAG, "Impersonation detected"); this.forceBackPressed(); return; } String url = uri.toString(); if (!AndroidacyUtil.isAndroidacyLink(url, uri)) { Log.w(TAG, "Calling non androidacy link in secure WebView: " + url); this.forceBackPressed(); return; } if (!Http.hasWebView()) { Log.w(TAG, "No WebView found to load url: " + url); this.forceBackPressed(); return; } Http.markCaptchaAndroidacySolved(); if (!url.contains(AndroidacyUtil.REFERRER)) { if (url.lastIndexOf('/') < url.lastIndexOf('?')) { url = url + '&' + AndroidacyUtil.REFERRER; } else { url = url + '?' + AndroidacyUtil.REFERRER; } } // Add token to url if not present String token = uri.getQueryParameter("token"); if (token == null) { // get from shared preferences token = MainApplication.getSharedPreferences().getString("pref_androidacy_api_token", null); url = url + "&token=" + token; } // Add device_id to url if not present String device_id = uri.getQueryParameter("device_id"); if (device_id == null) { // get from shared preferences try { device_id = AndroidacyRepoData.generateDeviceId(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } url = url + "&device_id=" + device_id; } 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); int compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0); this.setContentView(R.layout.webview); setActionBarBackground(null); this.setDisplayHomeAsUpEnabled(true); if (title == null || title.isEmpty()) { this.setTitle("Androidacy"); } else { this.setTitle(title); } if (allowInstall || title == null || title.isEmpty()) { this.hideActionBar(); } else { // Only used for note section if (config != null && !config.isEmpty()) { String configPkg = IntentHelper.getPackageOfConfig(config); try { XHooks.checkConfigTargetExists(this, configPkg, config); this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24, menu -> { IntentHelper.openConfig(this, config); return true; }); } catch (PackageManager.NameNotFoundException ignored) { } } } 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(); webSettings.setUserAgentString(Http.getAndroidacyUA()); webSettings.setDomStorageEnabled(true); webSettings.setJavaScriptEnabled(true); // Disable cache webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); webSettings.setAllowFileAccess(false); webSettings.setAllowContentAccess(false); // Attempt at fixing CloudFlare captcha. if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_CONTROL)) { WebSettingsCompat.setRequestedWithHeaderMode(webSettings, WebSettingsCompat.REQUESTED_WITH_HEADER_MODE_NO_HEADER); } // If API level is .= 33, allow setAlgorithmicDarkeningAllowed if (Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { try { webSettings.setAlgorithmicDarkeningAllowed(true); } catch (NoSuchMethodError ignored) { } } else { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Make website follow app theme webSettings.setForceDark(MainApplication.getINSTANCE().isLightTheme() ? WebSettings.FORCE_DARK_OFF : WebSettings.FORCE_DARK_ON); } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { // If api level is < 32, use force dark WebSettingsCompat.setForceDark(webSettings, MainApplication.getINSTANCE().isLightTheme() ? WebSettingsCompat.FORCE_DARK_OFF : WebSettingsCompat.FORCE_DARK_ON); } } this.webView.setWebViewClient(new WebViewClientCompat() { private String pageUrl; @Override public boolean shouldOverrideUrlLoading(@NonNull WebView view, @NonNull WebResourceRequest request) { // 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()); return true; } return false; } @Nullable @Override public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { if (AndroidacyActivity.this.megaIntercept(this.pageUrl, request.getUrl().toString())) { // Block request as Androidacy doesn't allow duplicate requests return new WebResourceResponse("text/plain", "UTF-8", new ByteArrayInputStream(new byte[0])); } return null; } @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { this.pageUrl = url; } @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) { if ((url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith("https://staging-api.androidacy.com/magisk/") || url.equals(pageUrl)) && (errorCode == 419 || errorCode == 429 || errorCode == 503)) { Toast.makeText(AndroidacyActivity.this, "Too many requests!", Toast.LENGTH_LONG).show(); AndroidacyActivity.this.runOnUiThread(AndroidacyActivity.this::onBackPressed); } else if (url.equals(this.pageUrl)) { postOnUiThread(() -> webViewNote.setVisibility(View.VISIBLE)); } } @Override public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { this.onReceivedError(failingUrl, errorCode); } @Override public void onReceivedError(@NonNull WebView view, @NonNull WebResourceRequest request, @NonNull WebResourceErrorCompat error) { if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { this.onReceivedError(request.getUrl().toString(), error.getErrorCode()); } } }); this.webView.setWebChromeClient(new WebChromeClient() { @Override public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, FileChooserParams fileChooserParams) { FoxActivity.getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent(), (code, data) -> filePathCallback.onReceiveValue(FileChooserParams.parseResult(code, data))); return true; } @Override public boolean onConsoleMessage(ConsoleMessage consoleMessage) { if (BuildConfig.DEBUG) { switch (consoleMessage.messageLevel()) { case TIP: Log.v(TAG, consoleMessage.message()); break; case LOG: Log.i(TAG, consoleMessage.message()); break; case WARNING: Log.w(TAG, consoleMessage.message()); break; case ERROR: Log.e(TAG, consoleMessage.message()); break; case DEBUG: Log.d(TAG, consoleMessage.message()); break; } } 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) { if (!androidacyWebAPI.downloadMode) { // Native module popup may cause download after consumed action if (androidacyWebAPI.consumedAction) return; // Workaround Androidacy bug final String moduleId = moduleIdOfUrl(downloadUrl); if (this.megaIntercept(webView.getUrl(), downloadUrl)) { // Block request as Androidacy doesn't allow duplicate requests return; } else if (moduleId != null) { // Download module Log.i(TAG, "megaIntercept failure. Forcing onBackPress"); this.onBackPressed(); } } androidacyWebAPI.consumedAction = true; androidacyWebAPI.downloadMode = false; } this.backOnResume = true; Log.i(TAG, "Exiting WebView " + AndroidacyUtil.hideToken(downloadUrl)); for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { if (downloadUrl.startsWith(prefix)) { return; } } IntentHelper.openCustomTab(this, downloadUrl); } }); this.androidacyWebAPI = new AndroidacyWebAPI(this, allowInstall); XHooks.onWebViewInitialize(this.webView, allowInstall); this.webView.addJavascriptInterface(this.androidacyWebAPI, "mmm"); if (compatLevel != 0) androidacyWebAPI.notifyCompatModeRaw(compatLevel); HashMap headers = new HashMap<>(); headers.put("Accept-Language", this.getResources().getConfiguration().locale.toLanguageTag()); if (BuildConfig.DEBUG) { headers.put("X-Debug", "true"); Log.i(TAG, "Debug mode enabled for webview using URL: " + url + " with headers: " + headers); } this.webView.loadUrl(url, headers); } @Override public void onBackPressed() { WebView webView = this.webView; if (webView != null && webView.canGoBack()) { webView.goBack(); } else { super.onBackPressed(); } } @Override protected void onResume() { super.onResume(); if (this.backOnResume) { this.backOnResume = false; this.forceBackPressed(); } else if (this.androidacyWebAPI != null) { this.androidacyWebAPI.consumedAction = false; } } private String moduleIdOfUrl(String url) { for (String prefix : new String[]{"https://production-api.androidacy.com/downloads/", "https://staging-api.androidacy.com/downloads/", "https://production-api.androidacy.com/magisk/readme/", "https://staging-api.androidacy.com/magisk/readme/", "https://prodiuction-api.androidacy.com/magisk/info/", "https://staging-api.androidacy.com/magisk/info/"}) { // Make both staging and non staging act the same int i = url.indexOf('?', prefix.length()); 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/downloads/", "https://staging-api.androidacy.com/downloads/"}) { // Make both staging and non staging act the same if (url.startsWith(prefix)) return true; } return false; } private boolean isDownloadUrl(String url) { for (String prefix : new String[]{"https://production-api.androidacy.com/magisk/downloads/", "https://staging-api.androidacy.com/magisk/downloads/"}) { // 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; String moduleId = AndroidacyUtil.getModuleId(fileUrl); if (moduleId == null) { Log.d(TAG, "No module id?"); // Re-open the page this.webView.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis()); } String checksum = AndroidacyUtil.getChecksumFromURL(fileUrl); String moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl); androidacyWebAPI.openNativeModuleDialogRaw(fileUrl, moduleId, moduleTitle, checksum, 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 { //noinspection UnusedAssignment module = null; this.runOnUiThread(() -> progressIndicator.setVisibility(View.INVISIBLE)); } this.backOnResume = true; this.downloadMode = false; return FileProvider.getUriForFile(this, this.getPackageName() + ".file-provider", this.moduleFile); } }