diff --git a/app/src/main/java/com/fox2code/mmm/NotificationType.kt b/app/src/main/java/com/fox2code/mmm/NotificationType.kt index 2f40014..2245f5a 100644 --- a/app/src/main/java/com/fox2code/mmm/NotificationType.kt +++ b/app/src/main/java/com/fox2code/mmm/NotificationType.kt @@ -1,5 +1,5 @@ @file:Suppress("KotlinConstantConditions", "UNINITIALIZED_ENUM_COMPANION_WARNING", - "ktConcatNullable", "BlockingMethodInNonBlockingContext" + "ktConcatNullable", "BlockingMethodInNonBlockingContext", "UnusedEquals" ) package com.fox2code.mmm @@ -160,7 +160,7 @@ enum class NotificationType constructor( compatActivity.cacheDir, "installer" + File.separator + "module.zip" ) - IntentHelper.openFileTo(compatActivity, module) { d: File, u: Uri, s: Int -> + IntentHelper.openFileTo(compatActivity, module, { d: File, u: Uri, s: Int -> if (s == IntentHelper.RESPONSE_FILE) { try { if (needPatch(d)) { @@ -202,7 +202,7 @@ enum class NotificationType constructor( InstallerInitializer.peekMagiskPath() == null ) } - } + }) }, false ) { diff --git a/app/src/main/java/com/fox2code/mmm/SetupActivity.kt b/app/src/main/java/com/fox2code/mmm/SetupActivity.kt index 08ed78d..069e06f 100644 --- a/app/src/main/java/com/fox2code/mmm/SetupActivity.kt +++ b/app/src/main/java/com/fox2code/mmm/SetupActivity.kt @@ -165,9 +165,9 @@ class SetupActivity : FoxActivity(), LanguageActivity { // Setup language selector val languageSelector = view.findViewById(R.id.setup_language_button) languageSelector.setOnClickListener { _: View? -> - val ls = LanguageSwitcher(Objects.requireNonNull(IntentHelper.getActivity(this))) - ls.setSupportedStringLocales(MainApplication.supportedLocales) - ls.showChangeLanguageDialog(IntentHelper.getActivity(this) as FragmentActivity) + val ls = IntentHelper.getActivity(this)?.let { LanguageSwitcher(it) } + ls?.setSupportedStringLocales(MainApplication.supportedLocales) + ls?.showChangeLanguageDialog(IntentHelper.getActivity(this) as FragmentActivity) } // Set up the buttons // Setup button diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java deleted file mode 100644 index 456f00d..0000000 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.java +++ /dev/null @@ -1,451 +0,0 @@ -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.net.http.SslError; -import android.os.Bundle; -import android.view.View; -import android.webkit.ConsoleMessage; -import android.webkit.CookieManager; -import android.webkit.SslErrorHandler; -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.swiperefreshlayout.widget.SwipeRefreshLayout; -import androidx.webkit.WebResourceErrorCompat; -import androidx.webkit.WebSettingsCompat; -import androidx.webkit.WebViewClientCompat; -import androidx.webkit.WebViewFeature; - -import com.fox2code.foxcompat.app.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.IntentHelper; -import com.fox2code.mmm.utils.io.net.Http; -import com.google.android.material.progressindicator.LinearProgressIndicator; - -import org.matomo.sdk.extra.TrackHelper; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Set; - -import timber.log.Timber; - -/** - * Per Androidacy repo implementation agreement, no request of this WebView shall be modified. - */ -public final class AndroidacyActivity extends FoxActivity { - - 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", "ClickableViewAccessibility"}) - protected void onCreate(@Nullable Bundle savedInstanceState) { - this.moduleFile = new File(this.getCacheDir(), "module.zip"); - super.onCreate(savedInstanceState); - TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().getTracker()); - Intent intent = this.getIntent(); - Uri uri; - if (!MainApplication.checkSecret(intent) || (uri = intent.getData()) == null) { - Timber.w("Impersonation detected"); - this.forceBackPressed(); - return; - } - String url = uri.toString(); - if (!AndroidacyUtil.isAndroidacyLink(url, uri)) { - Timber.w("Calling non androidacy link in secure WebView: %s", url); - this.forceBackPressed(); - return; - } - if (!Http.hasWebView()) { - Timber.w("No WebView found to load url: %s", url); - this.forceBackPressed(); - return; - } - // if action bar is shown, hide it - this.hideActionBar(); - 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 - url = url + "&token=" + AndroidacyRepoData.token; - } - // Add device_id to url if not present - String device_id = uri.getQueryParameter("device_id"); - if (device_id == null) { - // get from shared preferences - device_id = AndroidacyRepoData.generateDeviceId(); - url = url + "&device_id=" + device_id; - } - // check if client_id is present - String client_id = uri.getQueryParameter("client_id"); - if (client_id == null) { - // get from shared preferences - client_id = BuildConfig.ANDROIDACY_CLIENT_ID; - url = url + "&client_id=" + client_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); - WebSettings webSettings = this.webView.getSettings(); - webSettings.setUserAgentString(Http.getAndroidacyUA()); - CookieManager cookieManager = CookieManager.getInstance(); - cookieManager.setAcceptCookie(true); - cookieManager.setAcceptThirdPartyCookies(this.webView, true); - webSettings.setDomStorageEnabled(true); - webSettings.setJavaScriptEnabled(true); - webSettings.setCacheMode(WebSettings.LOAD_DEFAULT); - webSettings.setAllowFileAccess(false); - webSettings.setAllowContentAccess(false); - webSettings.setAllowFileAccessFromFileURLs(false); - webSettings.setAllowUniversalAccessFromFileURLs(false); - webSettings.setMediaPlaybackRequiresUserGesture(false); - // enable webview debugging on debug builds - if (BuildConfig.DEBUG) { - WebView.setWebContentsDebuggingEnabled(true); - } - // if app is in dark mode, force dark mode on webview - if (MainApplication.getINSTANCE().isDarkTheme()) { - // for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy - if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { - WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings, true); - } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { - WebSettingsCompat.setForceDark(webSettings, WebSettingsCompat.FORCE_DARK_ON); - } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { - WebSettingsCompat.setForceDarkStrategy(webSettings, WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY); - } - } - // Attempt at fixing CloudFlare captcha. - if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { - Set allowList = new HashSet<>(); - allowList.add("https://*.androidacy.com"); - WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings, allowList); - } - // get swipe to refresh layout - SwipeRefreshLayout swipeRefreshLayout = this.findViewById(R.id.swipe_refresh_layout); - - 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; - // sanitize url - String url = request.getUrl().toString(); - //noinspection UnnecessaryCallToStringValueOf - url = String.valueOf(AndroidacyUtil.hideToken(url)); - Timber.i("Exiting WebView %s", url); - 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) { - 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()); - } - } - - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - super.onReceivedSslError(view, handler, error); - // log the error and url of its request - Timber.tag("JSLog").e(error.toString()); - } - }); - // logic for swipe to refresh - swipeRefreshLayout.setOnRefreshListener(() -> { - swipeRefreshLayout.setRefreshing(false); - // reload page - webView.reload(); - }); - 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_HTTP) { - switch (consoleMessage.messageLevel()) { - case TIP -> Timber.tag("JSLog").i(consoleMessage.message()); - case LOG -> Timber.tag("JSLog").d(consoleMessage.message()); - case WARNING -> Timber.tag("JSLog").w(consoleMessage.message()); - case ERROR -> Timber.tag("JSLog").e(consoleMessage.message()); - default -> Timber.tag("JSLog").v(consoleMessage.message()); - } - } - return true; - } - - @Override - public void onProgressChanged(WebView view, int newProgress) { - if (downloadMode) return; - if (newProgress != 100 && progressIndicator.getVisibility() != View.VISIBLE) { - Timber.i("Progress: %d, showing progress bar", newProgress); - progressIndicator.setVisibility(View.VISIBLE); - } - // if progress is greater than one, set indeterminate to false - if (newProgress > 1) { - Timber.i("Progress: %d, setting indeterminate to false", newProgress); - progressIndicator.setIndeterminate(false); - } - progressIndicator.setProgressCompat(newProgress, true); - if (newProgress == 100 && progressIndicator.getVisibility() != View.INVISIBLE) { - Timber.i("Progress: %d, hiding progress bar", newProgress); - progressIndicator.setIndeterminate(true); - progressIndicator.setVisibility(View.GONE); - } - } - }); - 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 - Timber.i("megaIntercept failure. Forcing onBackPress"); - this.onBackPressed(); - } - } - androidacyWebAPI.consumedAction = true; - androidacyWebAPI.downloadMode = false; - } - this.backOnResume = true; - Timber.i("Exiting WebView %s", 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()); - // set layout to view - this.webView.loadUrl(url, headers); - } - - @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; - // ensure neither pageUrl nor fileUrl are going to cause a crash - if (pageUrl.contains(" ") || fileUrl.contains(" ")) return false; - if (!this.isFileUrl(fileUrl)) { - return false; - } - final AndroidacyWebAPI androidacyWebAPI = this.androidacyWebAPI; - String moduleId = AndroidacyUtil.getModuleId(fileUrl); - if (moduleId == null) { - Timber.i("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); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - if (webView != null) { - SwipeRefreshLayout parent = (SwipeRefreshLayout) webView.getParent(); - parent.removeView(webView); - webView.removeAllViews(); - webView.destroy(); // fix memory leak - } - Timber.i("onDestroy for %s", this); - } -} diff --git a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.kt b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.kt new file mode 100644 index 0000000..13db196 --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyActivity.kt @@ -0,0 +1,500 @@ +@file:Suppress("ktConcatNullable") + +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.net.http.SslError +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.webkit.ConsoleMessage +import android.webkit.ConsoleMessage.MessageLevel +import android.webkit.CookieManager +import android.webkit.DownloadListener +import android.webkit.SslErrorHandler +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.core.content.FileProvider +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.webkit.WebResourceErrorCompat +import androidx.webkit.WebSettingsCompat +import androidx.webkit.WebViewClientCompat +import androidx.webkit.WebViewFeature +import com.fox2code.foxcompat.app.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.Companion.checkConfigTargetExists +import com.fox2code.mmm.XHooks.Companion.onWebViewInitialize +import com.fox2code.mmm.utils.IntentHelper +import com.fox2code.mmm.utils.io.net.Http +import com.fox2code.mmm.utils.io.net.Http.Companion.androidacyUA +import com.fox2code.mmm.utils.io.net.Http.Companion.doHttpGet +import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView +import com.fox2code.mmm.utils.io.net.Http.Companion.markCaptchaAndroidacySolved +import com.google.android.material.progressindicator.LinearProgressIndicator +import org.matomo.sdk.extra.TrackHelper +import timber.log.Timber +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException + +/** + * Per Androidacy repo implementation agreement, no request of this WebView shall be modified. + */ +class AndroidacyActivity : FoxActivity() { + private var moduleFile: File? = null + @JvmField + var webView: WebView? = null + var webViewNote: TextView? = null + private var androidacyWebAPI: AndroidacyWebAPI? = null + var progressIndicator: LinearProgressIndicator? = null + @JvmField + var backOnResume = false + var downloadMode = false + @SuppressLint( + "SetJavaScriptEnabled", + "JavascriptInterface", + "RestrictedApi", + "ClickableViewAccessibility" + ) + override fun onCreate(savedInstanceState: Bundle?) { + moduleFile = File(this.cacheDir, "module.zip") + super.onCreate(savedInstanceState) + TrackHelper.track().screen(this).with(MainApplication.getINSTANCE().tracker) + val intent = this.intent + var uri: Uri? = intent.data + @Suppress("KotlinConstantConditions") + if (!MainApplication.checkSecret(intent) || intent.data.also { uri = it!! } == null) { + Timber.w("Impersonation detected") + forceBackPressed() + return + } + var url = uri.toString() + if (!AndroidacyUtil.isAndroidacyLink(url, uri!!)) { + Timber.w("Calling non androidacy link in secure WebView: %s", url) + forceBackPressed() + return + } + if (!hasWebView()) { + Timber.w("No WebView found to load url: %s", url) + forceBackPressed() + return + } + // if action bar is shown, hide it + hideActionBar() + markCaptchaAndroidacySolved() + if (!url.contains(AndroidacyUtil.REFERRER)) { + url = if (url.lastIndexOf('/') < url.lastIndexOf('?')) { + url + '&' + AndroidacyUtil.REFERRER + } else { + url + '?' + AndroidacyUtil.REFERRER + } + } + // Add token to url if not present + val token = uri!!.getQueryParameter("token") + if (token == null) { + // get from shared preferences + url = url + "&token=" + AndroidacyRepoData.token + } + // Add device_id to url if not present + var deviceId = uri!!.getQueryParameter("device_id") + if (deviceId == null) { + // get from shared preferences + deviceId = AndroidacyRepoData.generateDeviceId() + url = "$url&device_id=$deviceId" + } + // check if client_id is present + var clientId = uri!!.getQueryParameter("client_id") + if (clientId == null) { + // get from shared preferences + clientId = BuildConfig.ANDROIDACY_CLIENT_ID + url = "$url&client_id=$clientId" + } + val allowInstall = intent.getBooleanExtra(Constants.EXTRA_ANDROIDACY_ALLOW_INSTALL, false) + var title = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_TITLE) + val config = intent.getStringExtra(Constants.EXTRA_ANDROIDACY_ACTIONBAR_CONFIG) + val compatLevel = intent.getIntExtra(Constants.EXTRA_ANDROIDACY_COMPAT_LEVEL, 0) + this.setContentView(R.layout.webview) + setActionBarBackground(null) + setDisplayHomeAsUpEnabled(true) + if (title.isNullOrEmpty()) { + title = "Androidacy" + } + if (allowInstall || title.isEmpty()) { + hideActionBar() + } else { // Only used for note section + if (!config.isNullOrEmpty()) { + val configPkg = IntentHelper.getPackageOfConfig(config) + try { + checkConfigTargetExists(this, configPkg, config) + this.setActionBarExtraMenuButton(R.drawable.ic_baseline_app_settings_alt_24) { _: MenuItem? -> + IntentHelper.openConfig(this, config) + true + } + } catch (ignored: PackageManager.NameNotFoundException) { + } + } + } + val prgInd = findViewById(R.id.progress_bar) + prgInd.max = 100 + webView = findViewById(R.id.webView) + val wbv = webView + val webSettings = wbv?.settings + webSettings?.userAgentString = androidacyUA + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) + cookieManager.setAcceptThirdPartyCookies(webView, true) + webSettings?.domStorageEnabled = true + webSettings?.javaScriptEnabled = true + webSettings?.cacheMode = WebSettings.LOAD_DEFAULT + webSettings?.allowFileAccess = false + webSettings?.allowContentAccess = false + webSettings?.mediaPlaybackRequiresUserGesture = false + // enable webview debugging on debug builds + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + // if app is in dark mode, force dark mode on webview + if (MainApplication.getINSTANCE().isDarkTheme) { + // for api 33, use setAlgorithmicDarkeningAllowed, for api 29-32 use setForceDark, for api 28 and below use setForceDarkStrategy + if (WebViewFeature.isFeatureSupported(WebViewFeature.ALGORITHMIC_DARKENING)) { + WebSettingsCompat.setAlgorithmicDarkeningAllowed(webSettings!!, true) + } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { + @Suppress("DEPRECATION") + WebSettingsCompat.setForceDark(webSettings!!, WebSettingsCompat.FORCE_DARK_ON) + } else if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) { + @Suppress("DEPRECATION") + WebSettingsCompat.setForceDarkStrategy( + webSettings!!, + WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY + ) + } + } + // Attempt at fixing CloudFlare captcha. + if (WebViewFeature.isFeatureSupported(WebViewFeature.REQUESTED_WITH_HEADER_ALLOW_LIST)) { + val allowList: MutableSet = HashSet() + allowList.add("https://*.androidacy.com") + WebSettingsCompat.setRequestedWithHeaderOriginAllowList(webSettings!!, allowList) + } + // get swipe to refresh layout + val swipeRefreshLayout = findViewById(R.id.swipe_refresh_layout) + wbv?.webViewClient = object : WebViewClientCompat() { + private var pageUrl: String? = null + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + // Don't open non Androidacy urls inside WebView + if (request.isForMainFrame && !AndroidacyUtil.isAndroidacyLink(request.url)) { + if (downloadMode || backOnResume) return true + // sanitize url + @Suppress("NAME_SHADOWING") var url = request.url.toString() + url = AndroidacyUtil.hideToken(url).toString() + Timber.i("Exiting WebView %s", url) + IntentHelper.openUri(view.context, request.url.toString()) + return true + } + return false + } + + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + return if (megaIntercept(pageUrl, request.url.toString())) { + // Block request as Androidacy doesn't allow duplicate requests + WebResourceResponse("text/plain", "UTF-8", ByteArrayInputStream(ByteArray(0))) + } else null + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + pageUrl = url + } + + override fun onPageFinished(view: WebView, url: String) { + progressIndicator?.visibility = View.INVISIBLE + progressIndicator?.setProgressCompat(0, false) + } + + private fun onReceivedError(url: String, errorCode: Int) { + if (url.startsWith("https://production-api.androidacy.com/magisk/") || url.startsWith( + "https://staging-api.androidacy.com/magisk/" + ) || url == pageUrl && errorCode == 419 || errorCode == 429 || errorCode == 503 + ) { + Toast.makeText(this@AndroidacyActivity, "Too many requests!", Toast.LENGTH_LONG) + .show() + runOnUiThread { forceBackPressed() } + } else if (url == pageUrl) { + postOnUiThread { webViewNote!!.visibility = View.VISIBLE } + } + } + + @Deprecated("Deprecated in Java") + override fun onReceivedError( + view: WebView, + errorCode: Int, + description: String, + failingUrl: String + ) { + this.onReceivedError(failingUrl, errorCode) + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceErrorCompat + ) { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_RESOURCE_ERROR_GET_CODE)) { + this.onReceivedError(request.url.toString(), error.errorCode) + } + } + + override fun onReceivedSslError( + view: WebView, + handler: SslErrorHandler, + error: SslError + ) { + super.onReceivedSslError(view, handler, error) + // log the error and url of its request + Timber.tag("JSLog").e(error.toString()) + } + } + // logic for swipe to refresh + swipeRefreshLayout.setOnRefreshListener { + swipeRefreshLayout.isRefreshing = false + // reload page + wbv?.reload() + } + wbv?.webChromeClient = object : WebChromeClient() { + override fun onShowFileChooser( + webView: WebView, + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams + ): Boolean { + getFoxActivity(webView).startActivityForResult(fileChooserParams.createIntent()) { code: Int, data: Intent? -> + filePathCallback.onReceiveValue( + FileChooserParams.parseResult(code, data) + ) + } + return true + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + if (BuildConfig.DEBUG_HTTP) { + when (consoleMessage.messageLevel()) { + MessageLevel.TIP -> Timber.tag("JSLog").i(consoleMessage.message()) + MessageLevel.LOG -> Timber.tag("JSLog").d(consoleMessage.message()) + MessageLevel.WARNING -> Timber.tag("JSLog").w(consoleMessage.message()) + MessageLevel.ERROR -> Timber.tag("JSLog").e(consoleMessage.message()) + else -> Timber.tag("JSLog").v(consoleMessage.message()) + } + } + return true + } + + override fun onProgressChanged(view: WebView, newProgress: Int) { + if (downloadMode) return + if (newProgress != 100 && prgInd.visibility != View.VISIBLE) { + Timber.i("Progress: %d, showing progress bar", newProgress) + prgInd.visibility = View.VISIBLE + } + // if progress is greater than one, set indeterminate to false + if (newProgress > 1) { + Timber.i("Progress: %d, setting indeterminate to false", newProgress) + prgInd.isIndeterminate = false + } + prgInd.setProgressCompat(newProgress, true) + if (newProgress == 100 && prgInd.visibility != View.INVISIBLE) { + Timber.i("Progress: %d, hiding progress bar", newProgress) + prgInd.isIndeterminate = true + prgInd.visibility = View.GONE + } + } + } + wbv?.setDownloadListener(DownloadListener setDownloadListener@{ downloadUrl: String, _: String?, _: String?, _: String?, _: Long -> + if (downloadMode || isDownloadUrl(downloadUrl)) return@setDownloadListener + if (AndroidacyUtil.isAndroidacyLink(downloadUrl) && !backOnResume) { + val androidacyWebAPI = androidacyWebAPI + if (androidacyWebAPI != null) { + if (!androidacyWebAPI.downloadMode) { + // Native module popup may cause download after consumed action + if (androidacyWebAPI.consumedAction) return@setDownloadListener + // Workaround Androidacy bug + val moduleId = moduleIdOfUrl(downloadUrl) + if (megaIntercept(wbv.url, downloadUrl)) { + // Block request as Androidacy doesn't allow duplicate requests + return@setDownloadListener + } else if (moduleId != null) { + // Download module + Timber.i("megaIntercept failure. Forcing onBackPress") + forceBackPressed() + } + } + androidacyWebAPI.consumedAction = true + androidacyWebAPI.downloadMode = false + } + backOnResume = true + Timber.i("Exiting WebView %s", AndroidacyUtil.hideToken(downloadUrl)) + for (prefix in arrayOf( + "https://production-api.androidacy.com/downloads/", + "https://staging-api.androidacy.com/magisk/downloads/" + )) { + if (downloadUrl.startsWith(prefix)) { + return@setDownloadListener + } + } + IntentHelper.openCustomTab(this, downloadUrl) + } + }) + androidacyWebAPI = AndroidacyWebAPI(this, allowInstall) + onWebViewInitialize(webView, allowInstall) + wbv?.addJavascriptInterface(androidacyWebAPI!!, "mmm") + if (compatLevel != 0) androidacyWebAPI!!.notifyCompatModeRaw(compatLevel) + val headers = HashMap() + headers["Accept-Language"] = this.resources.configuration.locales.get(0).language + // set layout to view + wbv?.loadUrl(url, headers) + } + + override fun onResume() { + super.onResume() + if (backOnResume) { + backOnResume = false + forceBackPressed() + } else if (androidacyWebAPI != null) { + androidacyWebAPI!!.consumedAction = false + } + } + + private fun moduleIdOfUrl(url: String): String? { + for (prefix in arrayOf( + "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 + var i = url.indexOf('?', prefix.length) + if (i == -1) i = url.length + if (url.startsWith(prefix)) return url.substring(prefix.length, i) + } + if (isFileUrl(url)) { + val i = url.indexOf("&module=") + if (i != -1) { + val j = url.indexOf('&', i + 1) + return if (j == -1) { + url.substring(i + 8) + } else { + url.substring(i + 8, j) + } + } + } + return null + } + + private fun isFileUrl(url: String?): Boolean { + if (url == null) return false + for (prefix in arrayOf( + "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 fun isDownloadUrl(url: String): Boolean { + for (prefix in arrayOf( + "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 fun megaIntercept(pageUrl: String?, fileUrl: String?): Boolean { + if (pageUrl == null || fileUrl == null) return false + // ensure neither pageUrl nor fileUrl are going to cause a crash + if (pageUrl.contains(" ") || fileUrl.contains(" ")) return false + if (!isFileUrl(fileUrl)) { + return false + } + val androidacyWebAPI = androidacyWebAPI + val moduleId = AndroidacyUtil.getModuleId(fileUrl) + if (moduleId == null) { + Timber.i("No module id?") + // Re-open the page + webView!!.loadUrl(pageUrl + "&force_refresh=" + System.currentTimeMillis()) + } + val checksum = AndroidacyUtil.getChecksumFromURL(fileUrl) + val moduleTitle = AndroidacyUtil.getModuleTitle(fileUrl) + androidacyWebAPI!!.openNativeModuleDialogRaw( + fileUrl, + moduleId, + moduleTitle, + checksum, + androidacyWebAPI.canInstall() + ) + return true + } + + @Throws(IOException::class) + fun downloadFileAsync(url: String?): Uri { + downloadMode = true + runOnUiThread { + progressIndicator!!.isIndeterminate = false + progressIndicator!!.visibility = View.VISIBLE + } + var module: ByteArray? + try { + module = doHttpGet( + url!!, ({ downloaded: Int, total: Int, _: Boolean -> + progressIndicator!!.setProgressCompat( + downloaded * 100 / total, true) + + } as Http.ProgressListener?)!!) + FileOutputStream(moduleFile).use { fileOutputStream -> fileOutputStream.write(module) } + } finally { + module = null + runOnUiThread { progressIndicator!!.visibility = View.INVISIBLE } + } + backOnResume = true + downloadMode = false + @Suppress("ktConcatNullable") + return FileProvider.getUriForFile(this, this.packageName + ".file-provider", moduleFile!!) + } + + override fun onDestroy() { + super.onDestroy() + if (webView != null) { + val parent = webView!!.parent as SwipeRefreshLayout + parent.removeView(webView) + webView!!.removeAllViews() + webView!!.destroy() // fix memory leak + } + Timber.i("onDestroy for %s", this) + } + + companion object { + init { + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + } + } +} \ No newline at end of file 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 dba8ac2..08ba98d 100644 --- a/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java +++ b/app/src/main/java/com/fox2code/mmm/androidacy/AndroidacyWebAPI.java @@ -185,7 +185,7 @@ public class AndroidacyWebAPI { if (BuildConfig.DEBUG) Timber.d("Received openUrl request: %s", url); if (Objects.equals(Uri.parse(url).getScheme(), "https")) { - IntentHelper.openUrl(this.activity, url); + IntentHelper.Companion.openUrl(this.activity, url); } } @@ -261,7 +261,11 @@ public class AndroidacyWebAPI { if (this.effectiveCompatMode < 1) { if (!this.canInstall()) { this.downloadMode = true; - this.activity.runOnUiThread(() -> this.activity.webView.loadUrl(moduleUrl)); + this.activity.runOnUiThread(() -> { + if (this.activity.webView != null) { + this.activity.webView.loadUrl(moduleUrl); + } + }); } else { this.openNativeModuleDialogRaw(moduleUrl, moduleId, installTitle, checksum, true); } diff --git a/app/src/main/java/com/fox2code/mmm/background/BackgroundBootListener.kt b/app/src/main/java/com/fox2code/mmm/background/BackgroundBootListener.kt index d215316..bfb4c9e 100644 --- a/app/src/main/java/com/fox2code/mmm/background/BackgroundBootListener.kt +++ b/app/src/main/java/com/fox2code/mmm/background/BackgroundBootListener.kt @@ -10,7 +10,7 @@ class BackgroundBootListener : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { if (BOOT_COMPLETED != intent.action) return if (!MainApplication.isBackgroundUpdateCheckEnabled()) return - if (!Http.hasConnectivity()) return + if (!Http.hasConnectivity(context)) return // clear boot shared prefs MainApplication.getBootSharedPreferences().edit().clear().apply() synchronized(BackgroundUpdateChecker.lock) { 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 fade41b..21b5912 100644 --- a/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java +++ b/app/src/main/java/com/fox2code/mmm/module/ActionButtonType.java @@ -235,7 +235,7 @@ public enum ActionButtonType { name = moduleHolder.repoModule.moduleInfo.name; } TrackHelper.track().event("support_module", name).with(MainApplication.getINSTANCE().getTracker()); - IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().support); + IntentHelper.Companion.openUrl(button.getContext(), Objects.requireNonNull(moduleHolder.getMainModuleInfo().support)); } }, DONATE() { @Override @@ -254,7 +254,7 @@ public enum ActionButtonType { name = moduleHolder.repoModule.moduleInfo.name; } TrackHelper.track().event("donate_module", name).with(MainApplication.getINSTANCE().getTracker()); - IntentHelper.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate); + IntentHelper.Companion.openUrl(button.getContext(), moduleHolder.getMainModuleInfo().donate); } }, WARNING() { @Override 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 504872f..956e671 100644 --- a/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java +++ b/app/src/main/java/com/fox2code/mmm/repo/RepoManager.java @@ -345,7 +345,7 @@ public final class RepoManager extends SyncManager { } public boolean hasConnectivity() { - return Http.hasConnectivity(); + return Http.hasConnectivity(MainApplication.getINSTANCE().getApplicationContext()); } private RepoData addRepoData(String url, String fallBackName) { diff --git a/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.java b/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.java deleted file mode 100644 index 12b48cf..0000000 --- a/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.fox2code.mmm.settings; - -import android.content.Context; -import android.util.AttributeSet; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.Preference; -import androidx.preference.PreferenceViewHolder; - -public class LongClickablePreference extends Preference { - private OnPreferenceLongClickListener onPreferenceLongClickListener; - - public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - } - - public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - } - - public LongClickablePreference(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public LongClickablePreference(@NonNull Context context) { - super(context); - } - - @Override - public void onBindViewHolder(@NonNull PreferenceViewHolder holder) { - super.onBindViewHolder(holder); - holder.itemView.setOnLongClickListener(v -> performLongClick()); - } - - private boolean performLongClick() { - if (!this.isEnabled() || !this.isSelectable()) { - return false; - } - if (this.onPreferenceLongClickListener != null) { - return this.onPreferenceLongClickListener.onPreferenceLongClick(this); - } - return false; - } - - public void setOnPreferenceLongClickListener(OnPreferenceLongClickListener onPreferenceLongClickListener) { - this.onPreferenceLongClickListener = onPreferenceLongClickListener; - } - - public OnPreferenceLongClickListener getOnPreferenceLongClickListener() { - return this.onPreferenceLongClickListener; - } - - @FunctionalInterface - public interface OnPreferenceLongClickListener { - /** - * Called when a preference has been clicked. - * - * @param preference The preference that was clicked - * @return {@code true} if the click was handled - */ - boolean onPreferenceLongClick(@NonNull Preference preference); - } -} diff --git a/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.kt b/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.kt new file mode 100644 index 0000000..817fe9f --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/settings/LongClickablePreference.kt @@ -0,0 +1,52 @@ +package com.fox2code.mmm.settings + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder + +@Suppress("unused") +class LongClickablePreference : Preference { + var onPreferenceLongClickListener: OnPreferenceLongClickListener? = null + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int, + defStyleRes: Int + ) : super(context, attrs, defStyleAttr, defStyleRes) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context) : super(context) + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + holder.itemView.setOnLongClickListener { _: View? -> performLongClick() } + } + + private fun performLongClick(): Boolean { + if (!this.isEnabled || !this.isSelectable) { + return false + } + return if (onPreferenceLongClickListener != null) { + onPreferenceLongClickListener!!.onPreferenceLongClick(this) + } else false + } + + fun interface OnPreferenceLongClickListener { + /** + * Called when a preference has been clicked. + * + * @param preference The preference that was clicked + * @return `true` if the click was handled + */ + fun onPreferenceLongClick(preference: Preference): Boolean + } +} \ No newline at end of file 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 a261b19..2e85743 100644 --- a/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java +++ b/app/src/main/java/com/fox2code/mmm/settings/SettingsActivity.java @@ -224,16 +224,6 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // track all non empty values SharedPreferences sharedPreferences = dataStore.getSharedPreferences(); // disabled until EncryptedSharedPreferences fixes getAll() - /* StringBuilder keys = new StringBuilder(); - for (String key : sharedPreferences.getAll().keySet()) { - String value = sharedPreferences.getString(key, null); - if (value != null) { - keys.append(key).append(","); - } - } - if (keys.length() > 0) { - TrackHelper.track().event("prefs_all", keys.toString()).with(MainApplication.getINSTANCE().getTracker()); - }*/ // add bottom navigation bar to the settings BottomNavigationView bottomNavigationView = requireActivity().findViewById(R.id.bottom_navigation); if (bottomNavigationView != null) { @@ -726,7 +716,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; devModeStepFirstBootIgnore = true; - IntentHelper.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues"); + IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Androidacy/MagiskModuleManager/issues"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -768,7 +758,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { return true; } // build url from BuildConfig.REMOTE_URL and BuildConfig.COMMIT_HASH. May have to remove the .git at the end - IntentHelper.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH); + IntentHelper.Companion.openUrl(p.getContext(), finalUserRepo1.replace(".git", "") + "/tree/" + BuildConfig.COMMIT_HASH); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -780,7 +770,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // Next, the pref_androidacy_thanks should lead to the androidacy website linkClickable = findPreference("pref_androidacy_thanks"); linkClickable.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager"); + IntentHelper.Companion.openUrl(p.getContext(), "https://www.androidacy.com?utm_source=FoxMagiskModuleManager&utm_medium=app&utm_campaign=FoxMagiskModuleManager"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -792,7 +782,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // pref_fox2code_thanks should lead to https://github.com/Fox2Code linkClickable = findPreference("pref_fox2code_thanks"); linkClickable.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(p.getContext(), "https://github.com/Fox2Code"); + IntentHelper.Companion.openUrl(p.getContext(), "https://github.com/Fox2Code"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -855,7 +845,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { url = url.substring(0, url.length() - 4); } url += "/graphs/contributors"; - IntentHelper.openUrl(p.getContext(), url); + IntentHelper.Companion.openUrl(p.getContext(), url); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -873,7 +863,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { linkClickable = findPreference("pref_support"); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; - IntentHelper.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat"); + IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/Fox2Code_Chat"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -886,7 +876,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { linkClickable = findPreference("pref_announcements"); linkClickable.setOnPreferenceClickListener(p -> { devModeStep = 0; - IntentHelper.openUrl(p.getContext(), "https://t.me/androidacy"); + IntentHelper.Companion.openUrl(p.getContext(), "https://t.me/androidacy"); return true; }); linkClickable.setOnPreferenceLongClickListener(p -> { @@ -923,7 +913,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } if (ref.versionClicks == 7) { ref.versionClicks = 0; - IntentHelper.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ"); + IntentHelper.Companion.openUrl(p.getContext(), "https://www.youtube.com/watch?v=dQw4w9WgXcQ"); } return true; }); @@ -932,7 +922,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (!BuildConfig.FLAVOR.equals("play")) { pref_donate_fox.setOnPreferenceClickListener(p -> { // open fox - IntentHelper.openUrl(getFoxActivity(this), "https://paypal.me/fox2code"); + IntentHelper.Companion.openUrl(getFoxActivity(this), "https://paypal.me/fox2code"); return true; }); // handle long click on pref_donate_fox @@ -956,7 +946,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { clipboard.setPrimaryClip(ClipData.newPlainText(toastText, "FOX2CODE")); Toast.makeText(requireContext(), toastText, Toast.LENGTH_SHORT).show(); // open androidacy - IntentHelper.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"); + IntentHelper.Companion.openUrl(getFoxActivity(this), "https://www.androidacy.com/membership-join/?utm_source=foxmmm&utm_medium=app&utm_campaign=donate"); return true; }); // handle long click on pref_donate_androidacy @@ -1480,7 +1470,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { // set website, support, and submitmodule as well as donate if (repoData.getWebsite() != null) { findPreference(preferenceName + "_website").setOnPreferenceClickListener((preference1 -> { - IntentHelper.openUrl(getFoxActivity(this), repoData.getWebsite()); + IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getWebsite()); return true; })); } else { @@ -1488,7 +1478,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } if (repoData.getSupport() != null) { findPreference(preferenceName + "_support").setOnPreferenceClickListener((preference1 -> { - IntentHelper.openUrl(getFoxActivity(this), repoData.getSupport()); + IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSupport()); return true; })); } else { @@ -1496,7 +1486,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } if (repoData.getSubmitModule() != null) { findPreference(preferenceName + "_submit").setOnPreferenceClickListener((preference1 -> { - IntentHelper.openUrl(getFoxActivity(this), repoData.getSubmitModule()); + IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getSubmitModule()); return true; })); } else { @@ -1504,7 +1494,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { } if (repoData.getDonate() != null) { findPreference(preferenceName + "_donate").setOnPreferenceClickListener((preference1 -> { - IntentHelper.openUrl(getFoxActivity(this), repoData.getDonate()); + IntentHelper.Companion.openUrl(getFoxActivity(this), repoData.getDonate()); return true; })); } else { @@ -1543,7 +1533,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (!homepage.isEmpty()) { preference.setVisible(true); preference.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(getFoxActivity(this), homepage); + IntentHelper.Companion.openUrl(getFoxActivity(this), homepage); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { @@ -1563,7 +1553,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { preference.setVisible(true); preference.setIcon(ActionButtonType.supportIconForUrl(supportUrl)); preference.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(getFoxActivity(this), supportUrl); + IntentHelper.Companion.openUrl(getFoxActivity(this), supportUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { @@ -1583,7 +1573,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { preference.setVisible(true); preference.setIcon(ActionButtonType.donateIconForUrl(donateUrl)); preference.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(getFoxActivity(this), donateUrl); + IntentHelper.Companion.openUrl(getFoxActivity(this), donateUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { @@ -1602,7 +1592,7 @@ public class SettingsActivity extends FoxActivity implements LanguageActivity { if (submissionUrl != null && !submissionUrl.isEmpty()) { preference.setVisible(true); preference.setOnPreferenceClickListener(p -> { - IntentHelper.openUrl(getFoxActivity(this), submissionUrl); + IntentHelper.Companion.openUrl(getFoxActivity(this), submissionUrl); return true; }); ((LongClickablePreference) preference).setOnPreferenceLongClickListener(p -> { diff --git a/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.java b/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.java deleted file mode 100644 index 3270e2f..0000000 --- a/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.fox2code.mmm.settings; - -import android.content.SharedPreferences; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceDataStore; - -import java.util.Set; - -import timber.log.Timber; - -public class SharedPreferenceDataStore extends PreferenceDataStore { - - private final SharedPreferences mSharedPreferences; - - public SharedPreferenceDataStore(@NonNull SharedPreferences sharedPreferences) { - Timber.d("SharedPreferenceDataStore: %s", sharedPreferences); - mSharedPreferences = sharedPreferences; - } - - @NonNull - public SharedPreferences getSharedPreferences() { - Timber.d("getSharedPreferences: %s", mSharedPreferences); - return mSharedPreferences; - } - - @Override - public void putString(String key, @Nullable String value) { - mSharedPreferences.edit().putString(key, value).apply(); - } - - @Override - public void putStringSet(String key, @Nullable Set values) { - mSharedPreferences.edit().putStringSet(key, values).apply(); - } - - @Override - public void putInt(String key, int value) { - mSharedPreferences.edit().putInt(key, value).apply(); - } - - @Override - public void putLong(String key, long value) { - mSharedPreferences.edit().putLong(key, value).apply(); - } - - @Override - public void putFloat(String key, float value) { - mSharedPreferences.edit().putFloat(key, value).apply(); - } - - @Override - public void putBoolean(String key, boolean value) { - mSharedPreferences.edit().putBoolean(key, value).apply(); - } - - @Nullable - @Override - public String getString(String key, @Nullable String defValue) { - return mSharedPreferences.getString(key, defValue); - } - - @Nullable - @Override - public Set getStringSet(String key, @Nullable Set defValues) { - return mSharedPreferences.getStringSet(key, defValues); - } - - @Override - public int getInt(String key, int defValue) { - return mSharedPreferences.getInt(key, defValue); - } - - @Override - public long getLong(String key, long defValue) { - return mSharedPreferences.getLong(key, defValue); - } - - @Override - public float getFloat(String key, float defValue) { - return mSharedPreferences.getFloat(key, defValue); - } - - @Override - public boolean getBoolean(String key, boolean defValue) { - return mSharedPreferences.getBoolean(key, defValue); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.kt b/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.kt new file mode 100644 index 0000000..185938b --- /dev/null +++ b/app/src/main/java/com/fox2code/mmm/settings/SharedPreferenceDataStore.kt @@ -0,0 +1,68 @@ +package com.fox2code.mmm.settings + +import android.content.SharedPreferences +import androidx.preference.PreferenceDataStore +import timber.log.Timber + +class SharedPreferenceDataStore(sharedPreferences: SharedPreferences) : PreferenceDataStore() { + private val mSharedPreferences: SharedPreferences + + init { + Timber.d("SharedPreferenceDataStore: %s", sharedPreferences) + mSharedPreferences = sharedPreferences + } + + val sharedPreferences: SharedPreferences + get() { + Timber.d("getSharedPreferences: %s", mSharedPreferences) + return mSharedPreferences + } + + override fun putString(key: String, value: String?) { + mSharedPreferences.edit().putString(key, value).apply() + } + + override fun putStringSet(key: String, values: Set?) { + mSharedPreferences.edit().putStringSet(key, values).apply() + } + + override fun putInt(key: String, value: Int) { + mSharedPreferences.edit().putInt(key, value).apply() + } + + override fun putLong(key: String, value: Long) { + mSharedPreferences.edit().putLong(key, value).apply() + } + + override fun putFloat(key: String, value: Float) { + mSharedPreferences.edit().putFloat(key, value).apply() + } + + override fun putBoolean(key: String, value: Boolean) { + mSharedPreferences.edit().putBoolean(key, value).apply() + } + + override fun getString(key: String, defValue: String?): String? { + return mSharedPreferences.getString(key, defValue) + } + + override fun getStringSet(key: String, defValues: Set?): Set? { + return mSharedPreferences.getStringSet(key, defValues) + } + + override fun getInt(key: String, defValue: Int): Int { + return mSharedPreferences.getInt(key, defValue) + } + + override fun getLong(key: String, defValue: Long): Long { + return mSharedPreferences.getLong(key, defValue) + } + + override fun getFloat(key: String, defValue: Float): Float { + return mSharedPreferences.getFloat(key, defValue) + } + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + return mSharedPreferences.getBoolean(key, defValue) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.kt b/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.kt index e702a60..52a7c6b 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.kt +++ b/app/src/main/java/com/fox2code/mmm/utils/BudgetProgressDialog.kt @@ -1,47 +1,65 @@ -package com.fox2code.mmm.utils; +package com.fox2code.mmm.utils -import android.content.Context; -import android.content.res.Resources; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.ViewGroup; -import android.widget.ProgressBar; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.LinearLayoutCompat; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import android.content.Context +import android.util.TypedValue +import android.view.Gravity +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.LinearLayoutCompat +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlin.math.roundToInt // ProgressDialog is deprecated because it's an bad UX pattern, but sometimes we have no other choice... -public enum BudgetProgressDialog { +enum class BudgetProgressDialog { ; - public static AlertDialog build(Context context, String title, String message) { - Resources r = context.getResources(); - int padding = Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, r.getDisplayMetrics())); - LinearLayoutCompat v = new LinearLayoutCompat(context); - v.setOrientation(LinearLayoutCompat.HORIZONTAL); - ProgressBar pb = new ProgressBar(context); - v.addView(pb, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); - TextView t = new TextView(context); - t.setGravity(Gravity.CENTER); - v.addView(t, new LinearLayoutCompat.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, 4)); - v.setPadding(padding, padding, padding, padding); - - t.setText(message); - return new MaterialAlertDialogBuilder(context) - .setTitle(title) - .setView(v) - .setCancelable(false) - .create(); - } + companion object { + fun build(context: Context, title: String?, message: String?): AlertDialog { + val r = context.resources + val padding = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 20f, + r.displayMetrics + ).roundToInt() + val v = LinearLayoutCompat(context) + v.orientation = LinearLayoutCompat.HORIZONTAL + val pb = ProgressBar(context) + v.addView( + pb, + LinearLayoutCompat.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + ) + ) + val t = TextView(context) + t.gravity = Gravity.CENTER + v.addView( + t, + LinearLayoutCompat.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.MATCH_PARENT, + 4f + ) + ) + v.setPadding(padding, padding, padding, padding) + t.text = message + return MaterialAlertDialogBuilder(context) + .setTitle(title) + .setView(v) + .setCancelable(false) + .create() + } - public static AlertDialog build(Context context, int title, String message) { - return build(context, context.getString(title), message); - } + fun build(context: Context, title: Int, message: String?): AlertDialog { + return build(context, context.getString(title), message) + } - public static AlertDialog build(Context context, int title, int message) { - return build(context, title, context.getString(message)); - } -} + @JvmStatic + fun build(context: Context, title: Int, message: Int): AlertDialog { + return build(context, title, context.getString(message)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.kt b/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.kt index 8b2d374..7bd2e46 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.kt +++ b/app/src/main/java/com/fox2code/mmm/utils/ExternalHelper.kt @@ -1,115 +1,129 @@ -package com.fox2code.mmm.utils; +package com.fox2code.mmm.utils -import android.app.Dialog; -import android.content.ActivityNotFoundException; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.os.Bundle; -import android.widget.Toast; +import android.app.Dialog +import android.content.ActivityNotFoundException +import android.content.ComponentName +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import androidx.core.app.ActivityOptionsCompat +import androidx.core.util.Supplier +import com.fox2code.mmm.Constants +import com.topjohnwu.superuser.internal.UiThreadHandler +import timber.log.Timber -import androidx.appcompat.app.AlertDialog; -import androidx.core.app.ActivityOptionsCompat; -import androidx.core.util.Supplier; - -import com.fox2code.mmm.Constants; -import com.topjohnwu.superuser.internal.UiThreadHandler; - -import java.util.List; - -import timber.log.Timber; - -public final class ExternalHelper { - public static final ExternalHelper INSTANCE = new ExternalHelper(); - private static final boolean TEST_MODE = false; - private static final String FOX_MMM_OPEN_EXTERNAL = "com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL"; - private static final String FOX_MMM_EXTRA_REPO_ID = "extra_repo_id"; - private ComponentName fallback; - private CharSequence label; - private boolean multi; - - private ExternalHelper() { - } - - public void refreshHelper(Context context) { - Intent intent = new Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip")); - List resolveInfos = context.getPackageManager().queryIntentActivities(intent, PackageManager.GET_RESOLVED_FILTER); +class ExternalHelper private constructor() { + private var fallback: ComponentName? = null + private var label: CharSequence? = null + private var multi = false + fun refreshHelper(context: Context) { + val intent = Intent(FOX_MMM_OPEN_EXTERNAL, Uri.parse("https://fox2code.com/module.zip")) + val resolveInfos = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.packageManager.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of( + PackageManager.MATCH_DEFAULT_ONLY.toLong() + )) + } else { + @Suppress("DEPRECATION") + context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + } if (resolveInfos.isEmpty()) { - Timber.i("No external provider installed!"); - label = TEST_MODE ? "External" : null; - multi = TEST_MODE; - fallback = null; + Timber.i("No external provider installed!") + label = if (TEST_MODE) "External" else null + multi = TEST_MODE + fallback = null } else { - ResolveInfo resolveInfo = resolveInfos.get(0); - Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName); - fallback = new ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); - label = resolveInfo.loadLabel(context.getPackageManager()); - multi = resolveInfos.size() >= 2; + val resolveInfo = resolveInfos[0] + Timber.i("Found external provider: %s", resolveInfo.activityInfo.packageName) + fallback = + ComponentName(resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name) + label = resolveInfo.loadLabel(context.packageManager) + multi = resolveInfos.size >= 2 } } - public boolean openExternal(Context context, Uri uri, String repoId) { - if (label == null) - return false; - 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); + fun openExternal(context: Context, uri: Uri?, repoId: String?): Boolean { + if (label == null) return false + val param = + ActivityOptionsCompat.makeCustomAnimation(context, rikka.core.R.anim.fade_in, rikka.core.R.anim.fade_out) + .toBundle() + var intent = Intent(FOX_MMM_OPEN_EXTERNAL, uri) + intent.flags = IntentHelper.FLAG_GRANT_URI_PERMISSION + intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId) if (multi) { - intent = Intent.createChooser(intent, label); + intent = Intent.createChooser(intent, label) } else { - intent.putExtra(Constants.EXTRA_FADE_OUT, true); + intent.putExtra(Constants.EXTRA_FADE_OUT, true) } try { if (multi) { - context.startActivity(intent); + context.startActivity(intent) } else { - context.startActivity(intent, param); + context.startActivity(intent, param) } - return true; - } catch ( - ActivityNotFoundException e) { - Timber.e(e); + return true + } catch (e: ActivityNotFoundException) { + Timber.e(e) } if (fallback != null) { if (multi) { - intent = new Intent(FOX_MMM_OPEN_EXTERNAL, uri); - intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId); - intent.putExtra(Constants.EXTRA_FADE_OUT, true); + intent = Intent(FOX_MMM_OPEN_EXTERNAL, uri) + intent.putExtra(FOX_MMM_EXTRA_REPO_ID, repoId) + intent.putExtra(Constants.EXTRA_FADE_OUT, true) } - intent.setComponent(fallback); + intent.component = fallback try { - context.startActivity(intent, param); - return true; - } catch ( - ActivityNotFoundException e) { - Timber.e(e); + context.startActivity(intent, param) + return true + } catch (e: ActivityNotFoundException) { + Timber.e(e) } } - return false; + return false } - public void injectButton(AlertDialog.Builder builder, Supplier uriSupplier, String repoId) { - if (label == null) - return; - builder.setNeutralButton(label, (dialog, button) -> { - Context context = ((Dialog) dialog).getContext(); - 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(); + fun injectButton(builder: AlertDialog.Builder, uriSupplier: Supplier, repoId: String?) { + if (label == null) return + builder.setNeutralButton(label) { dialog: DialogInterface, _: Int -> + val context = (dialog as Dialog).context + object : Thread("Async downloader") { + override fun run() { + val uri = uriSupplier.get() + if (uri != null) { + UiThreadHandler.run { + if (!openExternal(context, uri, repoId)) { + Toast.makeText( + context, + "Failed to launch external activity", + Toast.LENGTH_SHORT + ).show() + } + } + } else { + UiThreadHandler.run { + Toast.makeText( + context, + "Failed to get download link", + Toast.LENGTH_SHORT + ).show() } - }); + } } - }.start(); - }); + }.start() + } + } + + companion object { + @JvmField + val INSTANCE = ExternalHelper() + private const val TEST_MODE = false + private const val FOX_MMM_OPEN_EXTERNAL = + "com.fox2code.mmm.utils.intent.action.OPEN_EXTERNAL" + private const val FOX_MMM_EXTRA_REPO_ID = "extra_repo_id" } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/FastException.kt b/app/src/main/java/com/fox2code/mmm/utils/FastException.kt index 4520ad6..02a5fcb 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/FastException.kt +++ b/app/src/main/java/com/fox2code/mmm/utils/FastException.kt @@ -1,15 +1,12 @@ -package com.fox2code.mmm.utils; +package com.fox2code.mmm.utils -import androidx.annotation.NonNull; - -public final class FastException extends RuntimeException { - public static final FastException INSTANCE = new FastException(); - - private FastException() {} +class FastException private constructor() : RuntimeException() { + @Synchronized + override fun fillInStackTrace(): Throwable { + return this + } - @NonNull - @Override - public synchronized Throwable fillInStackTrace() { - return this; + companion object { + val INSTANCE = FastException() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.kt b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.kt index 518601f..686d42a 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.kt +++ b/app/src/main/java/com/fox2code/mmm/utils/IntentHelper.kt @@ -1,395 +1,465 @@ -package com.fox2code.mmm.utils; +@file:Suppress("ktConcatNullable") -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.os.Environment; -import android.util.TypedValue; -import android.widget.Toast; +package com.fox2code.mmm.utils -import androidx.core.app.ActivityOptionsCompat; -import androidx.core.app.BundleCompat; +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.ContentResolver +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.util.TypedValue +import android.widget.Toast +import androidx.core.app.ActivityOptionsCompat +import androidx.core.app.BundleCompat +import com.fox2code.foxcompat.app.FoxActivity +import com.fox2code.mmm.BuildConfig +import com.fox2code.mmm.Constants +import com.fox2code.mmm.MainActivity +import com.fox2code.mmm.MainApplication +import com.fox2code.mmm.R +import com.fox2code.mmm.XHooks.Companion.getConfigIntent +import com.fox2code.mmm.XHooks.Companion.isModuleActive +import com.fox2code.mmm.androidacy.AndroidacyActivity +import com.fox2code.mmm.installer.InstallerActivity +import com.fox2code.mmm.markdown.MarkdownActivity +import com.fox2code.mmm.utils.io.Files.Companion.closeSilently +import com.fox2code.mmm.utils.io.Files.Companion.copy +import com.fox2code.mmm.utils.io.net.Http.Companion.hasWebView +import com.topjohnwu.superuser.CallbackList +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.io.SuFileInputStream +import timber.log.Timber +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.net.URISyntaxException -import com.fox2code.foxcompat.app.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.androidacy.AndroidacyActivity; -import com.fox2code.mmm.installer.InstallerActivity; -import com.fox2code.mmm.markdown.MarkdownActivity; -import com.fox2code.mmm.utils.io.Files; -import com.fox2code.mmm.utils.io.net.Http; -import com.topjohnwu.superuser.CallbackList; -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.io.SuFileInputStream; +enum class IntentHelper {; -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URISyntaxException; + companion object { -import timber.log.Timber; + interface OnFileReceivedCallback { + fun onReceived(target: File?, uri: Uri?, response: Int) + } + private const val EXTRA_TAB_SESSION = "android.support.customtabs.extra.SESSION" + private const val EXTRA_TAB_COLOR_SCHEME = "androidx.browser.customtabs.extra.COLOR_SCHEME" + private const val EXTRA_TAB_COLOR_SCHEME_DARK = 2 + private const val EXTRA_TAB_COLOR_SCHEME_LIGHT = 1 + private const val EXTRA_TAB_TOOLBAR_COLOR = "android.support.customtabs.extra.TOOLBAR_COLOR" + private const val EXTRA_TAB_EXIT_ANIMATION_BUNDLE = + "android.support.customtabs.extra.EXIT_ANIMATION_BUNDLE" + const val FLAG_GRANT_URI_PERMISSION = Intent.FLAG_GRANT_READ_URI_PERMISSION -public enum IntentHelper { - ; - private static final String EXTRA_TAB_SESSION = - "android.support.customtabs.extra.SESSION"; - private static final String EXTRA_TAB_COLOR_SCHEME = - "androidx.browser.customtabs.extra.COLOR_SCHEME"; - private static final int EXTRA_TAB_COLOR_SCHEME_DARK = 2; - private static final int EXTRA_TAB_COLOR_SCHEME_LIGHT = 1; - private static final String EXTRA_TAB_TOOLBAR_COLOR = - "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 = Intent.FLAG_GRANT_READ_URI_PERMISSION; + @JvmStatic + fun openUri(context: Context, uri: String) { + if (uri.startsWith("intent://")) { + try { + startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false) + } catch (e: URISyntaxException) { + Timber.e(e) + } catch (e: ActivityNotFoundException) { + Timber.e(e) + } + } else openUrl(context, uri) + } - public static void openUri(Context context, String uri) { - if (uri.startsWith("intent://")) { + @JvmOverloads + fun openUrl(context: Context, url: String?, forceBrowser: Boolean = false) { + Timber.d("Opening url: %s, forced browser %b", url, forceBrowser) try { - startActivity(context, Intent.parseUri(uri, Intent.URI_INTENT_SCHEME), false); - } catch (URISyntaxException | ActivityNotFoundException e) { - Timber.e(e); - } - } else openUrl(context, uri); - } - - public static void openUrl(Context context, String url) { - openUrl(context, url, false); - } - - public static void openUrl(Context context, String url, boolean forceBrowser) { - Timber.d("Opening url: %s, forced browser %b", url, forceBrowser); - try { - Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - myIntent.setFlags(FLAG_GRANT_URI_PERMISSION); - if (forceBrowser) { - myIntent.addCategory(Intent.CATEGORY_BROWSABLE); + val myIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + myIntent.flags = FLAG_GRANT_URI_PERMISSION + if (forceBrowser) { + myIntent.addCategory(Intent.CATEGORY_BROWSABLE) + } + startActivity(context, myIntent, false) + } catch (e: ActivityNotFoundException) { + Timber.d(e, "Could not find suitable activity to handle url") + Toast.makeText( + context, FoxActivity.getFoxActivity(context).getString( + R.string.no_browser + ), Toast.LENGTH_LONG + ).show() } - startActivity(context, myIntent, false); - } catch (ActivityNotFoundException e) { - Timber.d(e, "Could not find suitable activity to handle url"); - Toast.makeText(context, FoxActivity.getFoxActivity(context).getString( - R.string.no_browser), Toast.LENGTH_LONG).show(); } - } - public static void openCustomTab(Context context, String url) { - Timber.d("Opening url: %s in custom tab", url); - try { - Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - 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) { - Timber.d(e, "Could not find suitable activity to handle url"); - Toast.makeText(context, FoxActivity.getFoxActivity(context).getString( - R.string.no_browser), Toast.LENGTH_LONG).show(); + @JvmStatic + fun openCustomTab(context: Context, url: String?) { + Timber.d("Opening url: %s in custom tab", url) + try { + val viewIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + viewIntent.flags = FLAG_GRANT_URI_PERMISSION + val tabIntent = Intent(viewIntent) + tabIntent.flags = FLAG_GRANT_URI_PERMISSION + tabIntent.addCategory(Intent.CATEGORY_BROWSABLE) + startActivityEx(context, tabIntent, viewIntent) + } catch (e: ActivityNotFoundException) { + Timber.d(e, "Could not find suitable activity to handle url") + Toast.makeText( + context, FoxActivity.getFoxActivity(context).getString( + R.string.no_browser + ), Toast.LENGTH_LONG + ).show() + } } - } - 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) { - if (!Http.hasWebView()) { - Timber.w("Using custom tab for: %s", url); - openCustomTab(context, url); - return; - } - Uri uri = Uri.parse(url); - try { - Intent myIntent = new Intent( + @JvmStatic + @JvmOverloads + fun openUrlAndroidacy( + context: Context, + url: String?, + allowInstall: Boolean, + title: String? = null, + config: String? = null + ) { + if (!hasWebView()) { + Timber.w("Using custom tab for: %s", url) + openCustomTab(context, url) + return + } + val uri = Uri.parse(url) + try { + val myIntent = 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); - startActivity(context, myIntent, true); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, "No application can handle this request." - + " Please install a web-browser", Toast.LENGTH_SHORT).show(); + uri, + context, + AndroidacyActivity::class.java + ) + 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) + startActivity(context, myIntent, true) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + context, + "No application can handle this request." + " Please install a web-browser", + Toast.LENGTH_SHORT + ).show() + } } - } - public static String getPackageOfConfig(String config) { - int i = config.indexOf(' '); - if (i != -1) - config = config.substring(0, i); - i = config.indexOf('/'); - if (i != -1) - config = config.substring(0, i); - return config; - } + @Suppress("NAME_SHADOWING") + @JvmStatic + fun getPackageOfConfig(config: String): String { + var config = config + var i = config.indexOf(' ') + if (i != -1) config = config.substring(0, i) + i = config.indexOf('/') + if (i != -1) config = config.substring(0, i) + return config + } - public static void openConfig(Context context, String config) { - String pkg = getPackageOfConfig(config); - try { - Intent intent = XHooks.getConfigIntent(context, pkg, config); - if (intent == null) { - if ("org.lsposed.manager".equals(config) && ( - XHooks.isModuleActive("riru_lsposed") || - XHooks.isModuleActive("zygisk_lsposed"))) { - Shell.getShell().newJob().add( - "am start -a android.intent.action.MAIN " + - "-c org.lsposed.manager.LAUNCH_MANAGER " + - "com.android.shell/.BugreportWarningActivity") - .to(new CallbackList<>() { - @Override - public void onAddElement(String str) { - Timber.i("LSPosed: %s", str); + @JvmStatic + fun openConfig(context: Context, config: String) { + val pkg = getPackageOfConfig(config) + try { + var intent = getConfigIntent(context, pkg, config) + if (intent == null) { + if ("org.lsposed.manager" == config && (isModuleActive("riru_lsposed") || isModuleActive( + "zygisk_lsposed" + )) + ) { + Shell.getShell().newJob().add( + "am start -a android.intent.action.MAIN " + "-c org.lsposed.manager.LAUNCH_MANAGER " + "com.android.shell/.BugreportWarningActivity" + ).to(object : CallbackList() { + override fun onAddElement(str: String?) { + Timber.i("LSPosed: %s", str) } - }).submit(); - return; + }).submit() + return + } + intent = Intent("android.intent.action.APPLICATION_PREFERENCES") + intent.setPackage(pkg) } - intent = new Intent("android.intent.action.APPLICATION_PREFERENCES"); - intent.setPackage(pkg); + intent.putExtra(Constants.EXTRA_FROM_MANAGER, true) + startActivity(context, intent, false) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + context, "Failed to launch module config activity", Toast.LENGTH_SHORT + ).show() } - intent.putExtra(Constants.EXTRA_FROM_MANAGER, true); - startActivity(context, intent, false); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, - "Failed to launch module config activity", Toast.LENGTH_SHORT).show(); } - } - public static void openMarkdown(Context context, String url, String title, String config, Boolean changeBoot, Boolean needsRamdisk,int minMagisk, int minApi, int maxApi) { - try { - Intent intent = new Intent(context, MarkdownActivity.class); - MainApplication.addSecret(intent); - intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url); - intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title); - intent.putExtra(Constants.EXTRA_MARKDOWN_CHANGE_BOOT, changeBoot); - intent.putExtra(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK, needsRamdisk); - intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_MAGISK, minMagisk); - intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_API, minApi); - intent.putExtra(Constants.EXTRA_MARKDOWN_MAX_API, maxApi); - if (config != null && !config.isEmpty()) - intent.putExtra(Constants.EXTRA_MARKDOWN_CONFIG, config); - startActivity(context, intent, true); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, - "Failed to launch markdown activity", Toast.LENGTH_SHORT).show(); + @JvmStatic + fun openMarkdown( + context: Context, + url: String?, + title: String?, + config: String?, + changeBoot: Boolean?, + needsRamdisk: Boolean?, + minMagisk: Int, + minApi: Int, + maxApi: Int + ) { + try { + val intent = Intent(context, MarkdownActivity::class.java) + MainApplication.addSecret(intent) + intent.putExtra(Constants.EXTRA_MARKDOWN_URL, url) + intent.putExtra(Constants.EXTRA_MARKDOWN_TITLE, title) + intent.putExtra(Constants.EXTRA_MARKDOWN_CHANGE_BOOT, changeBoot) + intent.putExtra(Constants.EXTRA_MARKDOWN_NEEDS_RAMDISK, needsRamdisk) + intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_MAGISK, minMagisk) + intent.putExtra(Constants.EXTRA_MARKDOWN_MIN_API, minApi) + intent.putExtra(Constants.EXTRA_MARKDOWN_MAX_API, maxApi) + if (!config.isNullOrEmpty()) intent.putExtra( + Constants.EXTRA_MARKDOWN_CONFIG, config + ) + startActivity(context, intent, true) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + context, "Failed to launch markdown activity", Toast.LENGTH_SHORT + ).show() + } } - } - public static void openInstaller(Context context, String url, String title, String config, - String checksum, boolean mmtReborn) { - openInstaller(context, url, title, config, checksum, mmtReborn, false); - } - - public static void openInstaller(Context context, String url, String title, String config, - String checksum, boolean mmtReborn, boolean testDebug) { - try { - Intent intent = new Intent(context, InstallerActivity.class); - intent.setAction(Constants.INTENT_INSTALL_INTERNAL); - MainApplication.addSecret(intent); - intent.putExtra(Constants.EXTRA_INSTALL_PATH, url); - intent.putExtra(Constants.EXTRA_INSTALL_NAME, title); - if (config != null && !config.isEmpty()) - intent.putExtra(Constants.EXTRA_INSTALL_CONFIG, config); - if (checksum != null && !checksum.isEmpty()) - intent.putExtra(Constants.EXTRA_INSTALL_CHECKSUM, checksum); - if (mmtReborn) // Allow early styling of install process - intent.putExtra(Constants.EXTRA_INSTALL_MMT_REBORN, true); - if (testDebug && BuildConfig.DEBUG) - intent.putExtra(Constants.EXTRA_INSTALL_TEST_ROOTLESS, true); - startActivity(context, intent, true); - } catch (ActivityNotFoundException e) { - Toast.makeText(context, - "Failed to launch markdown activity", Toast.LENGTH_SHORT).show(); + @JvmStatic + @JvmOverloads + fun openInstaller( + context: Context, + url: String?, + title: String?, + config: String?, + checksum: String?, + mmtReborn: Boolean, + testDebug: Boolean = false + ) { + try { + val intent = Intent(context, InstallerActivity::class.java) + intent.action = Constants.INTENT_INSTALL_INTERNAL + MainApplication.addSecret(intent) + intent.putExtra(Constants.EXTRA_INSTALL_PATH, url) + intent.putExtra(Constants.EXTRA_INSTALL_NAME, title) + if (!config.isNullOrEmpty()) intent.putExtra( + Constants.EXTRA_INSTALL_CONFIG, config + ) + if (!checksum.isNullOrEmpty()) intent.putExtra( + Constants.EXTRA_INSTALL_CHECKSUM, checksum + ) + if (mmtReborn) // Allow early styling of install process + intent.putExtra(Constants.EXTRA_INSTALL_MMT_REBORN, true) + if (testDebug && BuildConfig.DEBUG) intent.putExtra( + Constants.EXTRA_INSTALL_TEST_ROOTLESS, true + ) + startActivity(context, intent, true) + } catch (e: ActivityNotFoundException) { + Toast.makeText( + context, "Failed to launch markdown activity", Toast.LENGTH_SHORT + ).show() + } } - } - - public static void startActivity(Context context, Class activityClass) { - startActivity(context, new Intent(context, activityClass), true); - } - public static void startActivity(Context context, Intent intent,boolean sameApp) - throws ActivityNotFoundException { - if (sameApp) { - startActivityEx(context, intent, null); - } else { - startActivityEx(context, null, intent); + fun startActivity(context: Context, activityClass: Class?) { + startActivity(context, Intent(context, activityClass), true) } - } - public static void startActivityEx(Context context, Intent intent1,Intent intent2) - throws ActivityNotFoundException { - if (intent1 == null && intent2 == null) - throw new NullPointerException("No intent defined for activity!"); - changeFlags(intent1, true); - changeFlags(intent2, false); - Activity activity = getActivity(context); - Bundle param = ActivityOptionsCompat.makeCustomAnimation(context, - android.R.anim.fade_in, android.R.anim.fade_out).toBundle(); - if (activity == null) { - if (intent1 != null) { - try { - context.startActivity(intent1, param); - return; - } catch (ActivityNotFoundException e) { - if (intent2 == null) throw e; - } + @Throws(ActivityNotFoundException::class) + fun startActivity(context: Context, intent: Intent?, sameApp: Boolean) { + if (sameApp) { + startActivityEx(context, intent, null) + } else { + startActivityEx(context, null, intent) } - context.startActivity(intent2, param); - } else { - if (intent1 != null) { - // Support Custom Tabs as sameApp intent - if (intent1.hasCategory(Intent.CATEGORY_BROWSABLE)) { - if (!intent1.hasExtra(EXTRA_TAB_SESSION)) { - Bundle bundle = new Bundle(); - BundleCompat.putBinder(bundle, EXTRA_TAB_SESSION, null); - intent1.putExtras(bundle); + } + + @Throws(ActivityNotFoundException::class) + fun startActivityEx(context: Context, intent1: Intent?, intent2: Intent?) { + if (intent1 == null && intent2 == null) throw NullPointerException("No intent defined for activity!") + changeFlags(intent1, true) + changeFlags(intent2, false) + val activity = getActivity(context) + val param = ActivityOptionsCompat.makeCustomAnimation( + context, android.R.anim.fade_in, android.R.anim.fade_out + ).toBundle() + if (activity == null) { + if (intent1 != null) { + try { + context.startActivity(intent1, param) + return + } catch (e: ActivityNotFoundException) { + if (intent2 == null) throw e } - intent1.putExtra(IntentHelper.EXTRA_TAB_EXIT_ANIMATION_BUNDLE, param); - if (activity instanceof FoxActivity) { - TypedValue typedValue = new TypedValue(); - activity.getTheme().resolveAttribute( - android.R.attr.background, typedValue, true); - if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && - typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { - intent1.putExtra(IntentHelper.EXTRA_TAB_TOOLBAR_COLOR, typedValue.data); - intent1.putExtra(IntentHelper.EXTRA_TAB_COLOR_SCHEME, - ((FoxActivity) activity).isLightTheme() ? - IntentHelper.EXTRA_TAB_COLOR_SCHEME_LIGHT : - IntentHelper.EXTRA_TAB_COLOR_SCHEME_DARK); + } + context.startActivity(intent2, param) + } else { + if (intent1 != null) { + // Support Custom Tabs as sameApp intent + if (intent1.hasCategory(Intent.CATEGORY_BROWSABLE)) { + if (!intent1.hasExtra(EXTRA_TAB_SESSION)) { + val bundle = Bundle() + BundleCompat.putBinder(bundle, EXTRA_TAB_SESSION, null) + intent1.putExtras(bundle) + } + intent1.putExtra(EXTRA_TAB_EXIT_ANIMATION_BUNDLE, param) + if (activity is FoxActivity) { + val typedValue = TypedValue() + activity.getTheme().resolveAttribute( + android.R.attr.background, typedValue, true + ) + if (typedValue.type >= TypedValue.TYPE_FIRST_COLOR_INT && typedValue.type <= TypedValue.TYPE_LAST_COLOR_INT) { + intent1.putExtra(EXTRA_TAB_TOOLBAR_COLOR, typedValue.data) + intent1.putExtra( + EXTRA_TAB_COLOR_SCHEME, + if (activity.isLightTheme) EXTRA_TAB_COLOR_SCHEME_LIGHT else EXTRA_TAB_COLOR_SCHEME_DARK + ) + } } } + try { + intent1.putExtra(Constants.EXTRA_FADE_OUT, true) + activity.overridePendingTransition( + android.R.anim.fade_in, android.R.anim.fade_out + ) + activity.startActivity(intent1, param) + return + } catch (e: ActivityNotFoundException) { + if (intent2 == null) throw e + } } - try { - intent1.putExtra(Constants.EXTRA_FADE_OUT, true); - activity.overridePendingTransition( - android.R.anim.fade_in, android.R.anim.fade_out); - activity.startActivity(intent1, param); - return; - } catch (ActivityNotFoundException e) { - if (intent2 == null) throw e; - } + activity.startActivity(intent2, param) } - activity.startActivity(intent2, param); } - } - private static void changeFlags(Intent intent,boolean sameApp) { - if (intent == null) return; - int flags = intent.getFlags() & - ~(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NEW_DOCUMENT); - if (!sameApp) { - flags &= ~Intent.FLAG_ACTIVITY_MULTIPLE_TASK; - if (intent.getData() == null) { - flags |= Intent.FLAG_ACTIVITY_NEW_TASK; - } else { - flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + private fun changeFlags(intent: Intent?, sameApp: Boolean) { + if (intent == null) return + var flags = + intent.flags and (Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NEW_DOCUMENT).inv() + if (!sameApp) { + flags = flags and Intent.FLAG_ACTIVITY_MULTIPLE_TASK.inv() + flags = if (intent.data == null) { + flags or Intent.FLAG_ACTIVITY_NEW_TASK + } else { + flags or Intent.FLAG_ACTIVITY_NEW_DOCUMENT + } } + intent.flags = flags } - intent.setFlags(flags); - } - public static Activity getActivity(Context context) { - while (!(context instanceof Activity)) { - if (context instanceof ContextWrapper) { - context = ((ContextWrapper) context).getBaseContext(); - } else return null; + @Suppress("NAME_SHADOWING") + fun getActivity(context: Context?): Activity? { + var context = context + while (context !is Activity) { + context = if (context is ContextWrapper) { + context.baseContext + } else return null + } + return context } - return (Activity) context; - } - public static final int RESPONSE_ERROR = 0; - public static final int RESPONSE_FILE = 1; - public static final int RESPONSE_URL = 2; + private const val RESPONSE_ERROR = 0 + const val RESPONSE_FILE = 1 + const val RESPONSE_URL = 2 - @SuppressLint("SdCardPath") - public static void openFileTo(FoxActivity compatActivity, File destination, - OnFileReceivedCallback callback) { - File destinationFolder; - if (destination == null || (destinationFolder = destination.getParentFile()) == null || - (!destinationFolder.mkdirs() && !destinationFolder.isDirectory())) { - callback.onReceived(destination, null, RESPONSE_ERROR); - return; - } - Intent intent = new Intent(Intent.ACTION_GET_CONTENT).setType("application/zip"); - intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false); - intent.putExtra(Intent.EXTRA_LOCAL_ONLY, false); - intent.addCategory(Intent.CATEGORY_OPENABLE); - Bundle param = ActivityOptionsCompat.makeCustomAnimation(compatActivity, - android.R.anim.fade_in, android.R.anim.fade_out).toBundle(); - compatActivity.startActivityForResult(intent, param, (result, data) -> { - Uri uri = data == null ? null : data.getData(); - if (uri == null || (result == Activity.RESULT_CANCELED && !(( - ContentResolver.SCHEME_FILE.equals(uri.getScheme()) - && uri.getPath() != null && - (uri.getPath().startsWith("/sdcard/") || - uri.getPath().startsWith("/data/")) - ) || ContentResolver.SCHEME_ANDROID_RESOURCE.equals(uri.getScheme())))) { - callback.onReceived(destination, null, RESPONSE_ERROR); - return; - } - Timber.i("FilePicker returned %s", uri); - if ("http".equals(uri.getScheme()) || - "https".equals(uri.getScheme())) { - callback.onReceived(destination, uri, RESPONSE_URL); - return; + @SuppressLint("SdCardPath") + fun openFileTo( + compatActivity: FoxActivity, destination: File?, callback: OnFileReceivedCallback + ) { + var destinationFolder: File? = null + if ((destination == null) || (destination.parentFile.also { + destinationFolder = it + } == null) || (!destinationFolder?.mkdirs()!! && !destinationFolder!!.isDirectory)) { + callback.onReceived(destination, null, RESPONSE_ERROR) + return } - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme()) || - (result != Activity.RESULT_OK && result != Activity.RESULT_FIRST_USER)) { - Toast.makeText(compatActivity, - R.string.file_picker_wierd, - Toast.LENGTH_SHORT).show(); - } - InputStream inputStream = null; - OutputStream outputStream = null; - boolean success = false; - try { - if (ContentResolver.SCHEME_FILE.equals(uri.getScheme())) { - String path = uri.getPath(); - if (path.startsWith("/sdcard/")) { // Fix file paths - path = Environment.getExternalStorageDirectory() - .getAbsolutePath() + path.substring(7); + val intent = Intent(Intent.ACTION_GET_CONTENT).setType("application/zip") + intent.flags = intent.flags and Intent.FLAG_ACTIVITY_NEW_TASK.inv() + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false) + intent.putExtra(Intent.EXTRA_LOCAL_ONLY, false) + intent.addCategory(Intent.CATEGORY_OPENABLE) + val param = ActivityOptionsCompat.makeCustomAnimation( + compatActivity, android.R.anim.fade_in, android.R.anim.fade_out + ).toBundle() + compatActivity.startActivityForResult(intent, param) { result: Int, data: Intent? -> + val uri = data?.data + if (uri == null || result == Activity.RESULT_CANCELED && ContentResolver.SCHEME_FILE == uri.scheme && uri.path != null && (uri.path!!.startsWith( + "/sdcard/" + ) || uri.path!!.startsWith("/data/")) || ContentResolver.SCHEME_ANDROID_RESOURCE != uri.scheme + ) { + callback.onReceived(destination, null, RESPONSE_ERROR) + return@startActivityForResult + } + Timber.i("FilePicker returned %s", uri) + if ("http" == uri.scheme || "https" == uri.scheme) { + callback.onReceived(destination, uri, RESPONSE_URL) + return@startActivityForResult + } + if (ContentResolver.SCHEME_FILE == uri.scheme || result != Activity.RESULT_OK && result != Activity.RESULT_FIRST_USER) { + Toast.makeText( + compatActivity, R.string.file_picker_wierd, Toast.LENGTH_SHORT + ).show() + } + var inputStream: InputStream? = null + var outputStream: OutputStream? = null + var success = false + try { + if (ContentResolver.SCHEME_FILE == uri.scheme) { + var path = uri.path + if (path!!.startsWith("/sdcard/")) { // Fix file paths + path = + Environment.getExternalStorageDirectory().absolutePath + path.substring( + 7 + ) + } + inputStream = SuFileInputStream.open( + File(path).absoluteFile + ) + } else { + inputStream = compatActivity.contentResolver.openInputStream(uri) } - inputStream = SuFileInputStream.open( - new File(path).getAbsoluteFile()); - } else { - inputStream = compatActivity.getContentResolver() - .openInputStream(uri); + outputStream = FileOutputStream(destination) + copy(inputStream!!, outputStream) + Timber.i("File saved at %s", destination) + success = true + } catch (e: Exception) { + Timber.e(e) + Toast.makeText( + compatActivity, R.string.file_picker_failure, Toast.LENGTH_SHORT + ).show() + } finally { + closeSilently(inputStream) + closeSilently(outputStream) + if (!success && destination.exists() && !destination.delete()) Timber.e("Failed to delete artefact!") } - outputStream = new FileOutputStream(destination); - Files.copy(inputStream, outputStream); - Timber.i("File saved at %s", destination); - success = true; - } catch (Exception e) { - Timber.e(e); - Toast.makeText(compatActivity, - R.string.file_picker_failure, - Toast.LENGTH_SHORT).show(); - } finally { - Files.closeSilently(inputStream); - Files.closeSilently(outputStream); - if (!success && destination.exists() && !destination.delete()) - Timber.e("Failed to delete artefact!"); + callback.onReceived( + destination, uri, if (success) RESPONSE_FILE else RESPONSE_ERROR + ) } - callback.onReceived(destination, uri, success ? RESPONSE_FILE : RESPONSE_ERROR); - }); - } + } - public interface OnFileReceivedCallback { - void onReceived(File target,Uri uri,int response); + fun openFileTo( + compatActivity: FoxActivity?, + destination: File, + callback: (File, Uri, Int) -> Unit + ) { + openFileTo(compatActivity!!, destination, object : OnFileReceivedCallback { + override fun onReceived(target: File?, uri: Uri?, response: Int) { + if (response == RESPONSE_ERROR) { + MainActivity.getFoxActivity(compatActivity)?.runOnUiThread { + Toast.makeText( + compatActivity, R.string.no_file_provided, Toast.LENGTH_SHORT + ).show() + } + } else { + try { + callback(target!!, uri!!, response) + } catch (e: Exception) { + Timber.e(e) + compatActivity.forceBackPressed() + } + } + } + }) + } } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.kt b/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.kt index 6e75c9c..b84a6c6 100644 --- a/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.kt +++ b/app/src/main/java/com/fox2code/mmm/utils/io/net/Http.kt @@ -3,13 +3,12 @@ package com.fox2code.mmm.utils.io.net import android.annotation.SuppressLint -import android.app.Activity import android.content.Context import android.content.SharedPreferences +import android.net.ConnectivityManager +import android.net.NetworkCapabilities import android.net.Uri import android.os.Build -import android.os.Handler -import android.os.Looper import android.system.ErrnoException import android.system.Os import android.webkit.CookieManager @@ -24,7 +23,6 @@ import com.fox2code.mmm.androidacy.AndroidacyUtil import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskPath import com.fox2code.mmm.installer.InstallerInitializer.Companion.peekMagiskVersion import com.fox2code.mmm.utils.io.Files.Companion.makeBuffer -import com.google.android.material.snackbar.Snackbar import com.google.net.cronet.okhttptransport.CronetInterceptor import okhttp3.Cache import okhttp3.Dns @@ -57,11 +55,9 @@ import java.net.UnknownHostException import java.nio.charset.StandardCharsets import java.util.Objects import java.util.concurrent.TimeUnit -import javax.net.ssl.SSLException import kotlin.system.exitProcess -enum class Http { - ; +enum class Http {; interface ProgressListener { fun onUpdate(downloaded: Int, total: Int, done: Boolean) @@ -84,8 +80,9 @@ enum class Http { init { sharedPreferences = context.getSharedPreferences("mmm_dns", Context.MODE_PRIVATE) this.parent = parent - this.fallbacks = HashSet(listOf(*fallbacks)).toString().replaceAfter("]", "").replace("[", "") - .split(",").toHashSet() + this.fallbacks = + HashSet(listOf(*fallbacks)).toString().replaceAfter("]", "").replace("[", "") + .split(",").toHashSet() fallbackCache = HashMap() } @@ -103,8 +100,8 @@ enum class Http { hostname ) fallbackCache[hostname] = addresses - sharedPreferences.edit().putString(hostname.replace('.', '_'), toString(addresses)) - .apply() + sharedPreferences.edit() + .putString(hostname.replace('.', '_'), toString(addresses)).apply() } catch (e: UnknownHostException) { val key = sharedPreferences.getString(hostname.replace('.', '_'), "") if (key!!.isEmpty()) throw e @@ -180,11 +177,13 @@ enum class Http { } companion object { + private var limitedRetries: Int = 0 private var httpClient: OkHttpClient? = null private var httpClientDoH: OkHttpClient? = null private var httpClientWithCache: OkHttpClient? = null private var httpClientWithCacheDoH: OkHttpClient? = null private var fallbackDNS: FallBackDNS? = null + @JvmStatic var androidacyUA: String? = null private var hasWebView = false @@ -214,23 +213,18 @@ enum class Http { } catch (t: Exception) { Timber.e(t, "No WebView support!") // show a toast - val context: Context = - mainApplication.applicationContext + val context: Context = mainApplication.applicationContext MainActivity.getFoxActivity(context).runOnUiThread { Toast.makeText( - mainApplication, - R.string.error_creating_cookie_database, - Toast.LENGTH_LONG + mainApplication, R.string.error_creating_cookie_database, Toast.LENGTH_LONG ).show() } } // get webview version var webviewVersion = "0.0.0" - val pi = - WebViewCompat.getCurrentWebViewPackage(mainApplication) + val pi = WebViewCompat.getCurrentWebViewPackage(mainApplication) if (pi != null) { - webviewVersion = - pi.versionName + webviewVersion = pi.versionName } // webviewVersionMajor is the everything before the first dot val webviewVersionCode: Int @@ -243,14 +237,11 @@ enum class Http { } else { // use the first dot webviewVersion.substring( - 0, - dot + 0, dot ).toInt() } Timber.d( - "Webview version: %s (%d)", - webviewVersion, - webviewVersionCode + "Webview version: %s (%d)", webviewVersion, webviewVersionCode ) hasWebView = cookieManager != null && webviewVersionCode >= 83 // 83 is the first version Androidacy supports due to errors in 82 @@ -317,13 +308,10 @@ enum class Http { ) } } - if (chain.request() - .header("Accept-Language") == null - ) { + if (chain.request().header("Accept-Language") == null) { request.header( "Accept-Language", // Send system language to the server - mainApplication.resources - .configuration.locales.get(0).toLanguageTag() + mainApplication.resources.configuration.locales.get(0).toLanguageTag() ) } // add client hints @@ -331,16 +319,13 @@ enum class Http { request.header("Sec-CH-UA-Mobile", "?1") request.header("Sec-CH-UA-Platform", "Android") request.header( - "Sec-CH-UA-Platform-Version", - Build.VERSION.RELEASE + "Sec-CH-UA-Platform-Version", Build.VERSION.RELEASE ) request.header( - "Sec-CH-UA-Arch", - Build.SUPPORTED_ABIS[0] + "Sec-CH-UA-Arch", Build.SUPPORTED_ABIS[0] ) request.header( - "Sec-CH-UA-Full-Version", - BuildConfig.VERSION_NAME + "Sec-CH-UA-Full-Version", BuildConfig.VERSION_NAME ) request.header("Sec-CH-UA-Model", Build.DEVICE) request.header( @@ -370,20 +355,17 @@ enum class Http { builder.enableQuic(true) // Cache size is 10MB // Make the directory if it does not exist - val cacheDir = - File(mainApplication.cacheDir, "cronet") + val cacheDir = File(mainApplication.cacheDir, "cronet") if (!cacheDir.exists()) { if (!cacheDir.mkdirs()) { throw IOException("Failed to create cronet cache directory") } } builder.setStoragePath( - mainApplication.cacheDir - .absolutePath + "/cronet" + mainApplication.cacheDir.absolutePath + "/cronet" ) builder.enableHttpCache( - CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, - (10 * 1024 * 1024).toLong() + CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, (10 * 1024 * 1024).toLong() ) // Add quic hint builder.addQuicHint("github.com", 443, 443) @@ -418,32 +400,30 @@ enum class Http { "production-api.androidacy.com" ) httpclientBuilder.dns(Dns.SYSTEM) - httpClient = - followRedirects(httpclientBuilder, true).build() + httpClient = followRedirects(httpclientBuilder, true).build() followRedirects(httpclientBuilder, false).build() httpclientBuilder.dns(fallbackDNS!!) - httpClientDoH = - followRedirects(httpclientBuilder, true).build() + httpClientDoH = followRedirects(httpclientBuilder, true).build() followRedirects(httpclientBuilder, false).build() httpclientBuilder.cache( Cache( File( - mainApplication.cacheDir, - "http_cache" + mainApplication.cacheDir, "http_cache" ), 16L * 1024L * 1024L ) ) // 16Mib of cache httpclientBuilder.dns(Dns.SYSTEM) - httpClientWithCache = - followRedirects(httpclientBuilder, true).build() + httpClientWithCache = followRedirects(httpclientBuilder, true).build() httpclientBuilder.dns(fallbackDNS!!) - httpClientWithCacheDoH = - followRedirects(httpclientBuilder, true).build() + httpClientWithCacheDoH = followRedirects(httpclientBuilder, true).build() Timber.i("Initialized Http successfully!") doh = MainApplication.isDohEnabled() } - private fun followRedirects(builder: OkHttpClient.Builder, followRedirects: Boolean): OkHttpClient.Builder { + private fun followRedirects( + builder: OkHttpClient.Builder, + followRedirects: Boolean + ): OkHttpClient.Builder { return builder.followRedirects(followRedirects).followSslRedirects(followRedirects) } @@ -503,8 +483,7 @@ enum class Http { if (response.code != 200 && response.code != 204 && (response.code != 304 || !allowCache)) { Timber.e( "Failed to fetch " + url.replace( - "=[^&]*".toRegex(), - "=****" + "=[^&]*".toRegex(), "=****" ) + " with code " + response.code ) checkNeedCaptchaAndroidacy(url, response.code) @@ -513,6 +492,34 @@ enum class Http { // Regenerate the token throw HttpException("Androidacy token is invalid", 401) } + if (response.code == 429) { + val retryAfter = response.header("Retry-After") + if (retryAfter != null) { + try { + val seconds = Integer.parseInt(retryAfter) + Timber.d("Sleeping for $seconds seconds") + Thread.sleep(seconds * 1000L) + } catch (e: NumberFormatException) { + Timber.e(e, "Failed to parse Retry-After header") + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + } else {// start with one second and try up to five times + if (limitedRetries < 5) { + limitedRetries++ + Timber.d("Sleeping for 1 second") + try { + Thread.sleep(1000L * limitedRetries) + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + return doHttpGet(url, allowCache) + } else { + Timber.e("Failed to fetch " + url + ", code: " + response.code) + throw HttpException(response.code) + } + } + } throw HttpException(response.code) } } @@ -566,6 +573,34 @@ enum class Http { if (response.code != 200 && response.code != 204 && (response.code != 304 || !allowCache)) { if (BuildConfig.DEBUG_HTTP) Timber.e("Failed to fetch " + url + ", code: " + response.code + ", body: " + response.body.string()) checkNeedCaptchaAndroidacy(url, response.code) + if (response.code == 429) { + val retryAfter = response.header("Retry-After") + if (retryAfter != null) { + try { + val seconds = Integer.parseInt(retryAfter) + Timber.d("Sleeping for $seconds seconds") + Thread.sleep(seconds * 1000L) + } catch (e: NumberFormatException) { + Timber.e(e, "Failed to parse Retry-After header") + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + } else {// start with one second and try up to five times + if (limitedRetries < 5) { + limitedRetries++ + Timber.d("Sleeping for 1 second") + try { + Thread.sleep(1000L * limitedRetries) + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + return doHttpPostRaw(url, data, allowCache) + } else { + Timber.e("Failed to fetch " + url + ", code: " + response.code) + throw HttpException(response.code) + } + } + } throw HttpException(response.code) } var responseBody = response.body @@ -580,11 +615,40 @@ enum class Http { @JvmStatic @Throws(IOException::class) fun doHttpGet(url: String, progressListener: ProgressListener): ByteArray { - val response = getHttpClient()!! - .newCall(Request.Builder().url(url).get().build()).execute() + val response = + getHttpClient()!!.newCall(Request.Builder().url(url).get().build()).execute() if (response.code != 200 && response.code != 204) { Timber.e("Failed to fetch " + url + ", code: " + response.code) checkNeedCaptchaAndroidacy(url, response.code) + // if error is 429, exponential backoff + if (response.code == 429) { + val retryAfter = response.header("Retry-After") + if (retryAfter != null) { + try { + val seconds = Integer.parseInt(retryAfter) + Timber.d("Sleeping for $seconds seconds") + Thread.sleep(seconds * 1000L) + } catch (e: NumberFormatException) { + Timber.e(e, "Failed to parse Retry-After header") + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + } else {// start with one second and try up to five times + if (limitedRetries < 5) { + limitedRetries++ + Timber.d("Sleeping for 1 second") + try { + Thread.sleep(1000L * limitedRetries) + } catch (e: InterruptedException) { + Timber.e(e, "Failed to sleep") + } + return doHttpGet(url, progressListener) + } else { + Timber.e("Failed to fetch " + url + ", code: " + response.code) + throw HttpException(response.code) + } + } + } throw HttpException(response.code) } val responseBody = Objects.requireNonNull(response.body) @@ -611,17 +675,13 @@ enum class Http { if (nextUpdate < currentUpdate) { nextUpdate = currentUpdate + updateInterval progressListener.onUpdate( - (downloaded / divider).toInt(), - (target / divider).toInt(), - false + (downloaded / divider).toInt(), (target / divider).toInt(), false ) } } inputStream.close() progressListener.onUpdate( - (downloaded / divider).toInt(), - (target / divider).toInt(), - true + (downloaded / divider).toInt(), (target / divider).toInt(), true ) return byteArrayOutputStream.toByteArray() } @@ -654,36 +714,16 @@ enum class Http { } @JvmStatic - fun hasConnectivity(): Boolean { - // Check if we have internet connection - Timber.d("Checking internet connection...") - // this url is actually hosted by Cloudflare and is not dependent on Androidacy servers being up - val resp: ByteArray = try { - doHttpGet("https://production-api.androidacy.com/cdn-cgi/trace", false) - } catch (e: Exception) { - Timber.e(e, "Failed to check internet connection. Assuming no internet connection.") - // check if it's a security or ssl exception - if (e is SSLException || e is SecurityException) { - // if it is, user installed a certificate that blocks the connection - // show a snackbar to inform the user - val context: Activity? = MainApplication.getINSTANCE().lastCompatActivity - Handler(Looper.getMainLooper()).post { - if (context != null) { - Snackbar.make( - context.findViewById(android.R.id.content), - R.string.certificate_error, - Snackbar.LENGTH_LONG - ).show() - } - } - } - return false - } - // get the response body - val response = String(resp, StandardCharsets.UTF_8) - // check if the response body contains "visit_scheme=https" and "http/" - // if it does, we have internet connection - return response.contains("visit_scheme=https") && response.contains("http/") + fun hasConnectivity(context: Context): Boolean { + // Check if we have internet connection using connectivity manager + val connectivityManager = + context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + // are we connected to a network with internet capabilities? + val networkCapabilities = + connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork) + return networkCapabilities != null && networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_INTERNET + ) } } } \ No newline at end of file