package com.fox2code.mmm; import android.Manifest; import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.text.InputType; import android.util.Log; import android.util.TypedValue; import android.view.View; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; import android.widget.CheckBox; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.widget.PopupMenu; import androidx.appcompat.widget.SearchView; import androidx.cardview.widget.CardView; import androidx.core.app.ActivityCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.content.ContextCompat; import androidx.core.graphics.ColorUtils; import androidx.preference.PreferenceManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.fox2code.foxcompat.FoxActivity; import com.fox2code.foxcompat.FoxDisplay; import com.fox2code.mmm.background.BackgroundUpdateChecker; import com.fox2code.mmm.installer.InstallerInitializer; import com.fox2code.mmm.manager.LocalModuleInfo; import com.fox2code.mmm.manager.ModuleManager; import com.fox2code.mmm.module.ModuleViewAdapter; import com.fox2code.mmm.module.ModuleViewListBuilder; import com.fox2code.mmm.repo.RepoManager; import com.fox2code.mmm.settings.SettingsActivity; import com.fox2code.mmm.utils.BlurUtils; import com.fox2code.mmm.utils.ExternalHelper; import com.fox2code.mmm.utils.Http; import com.fox2code.mmm.utils.IntentHelper; import com.google.android.material.button.MaterialButton; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.materialswitch.MaterialSwitch; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.topjohnwu.superuser.internal.UiThreadHandler; import org.chromium.net.ExperimentalCronetEngine; import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; import java.util.Objects; import eightbitlab.com.blurview.BlurView; public class MainActivity extends FoxActivity implements SwipeRefreshLayout.OnRefreshListener, SearchView.OnQueryTextListener, SearchView.OnCloseListener, OverScrollManager.OverScrollHelper { private static final String TAG = "MainActivity"; private static final int PRECISION = 10000; public static boolean doSetupNowRunning = true; public final ModuleViewListBuilder moduleViewListBuilder; public LinearProgressIndicator progressIndicator; private ModuleViewAdapter moduleViewAdapter; private SwipeRefreshLayout swipeRefreshLayout; private int swipeRefreshLayoutOrigStartOffset; private int swipeRefreshLayoutOrigEndOffset; private long swipeRefreshBlocker = 0; private int overScrollInsetTop; private int overScrollInsetBottom; private TextView actionBarPadding; private BlurView actionBarBlur; private ColorDrawable actionBarBackground; private RecyclerView moduleList; private CardView searchCard; private SearchView searchView; private boolean initMode; private boolean doSetupRestarting = false; private boolean urlFactoryInstalled = false; public MainActivity() { this.moduleViewListBuilder = new ModuleViewListBuilder(this); this.moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); } @Override protected void onResume() { BackgroundUpdateChecker.onMainActivityResume(this); super.onResume(); } @Override protected void onCreate(Bundle savedInstanceState) { this.initMode = true; // Ensure HTTP Cache directories are created Http.ensureCacheDirs(this); if (!urlFactoryInstalled) { try { ExperimentalCronetEngine cronetEngine = new ExperimentalCronetEngine.Builder(this).build(); CronetURLStreamHandlerFactory cronetURLStreamHandlerFactory = new CronetURLStreamHandlerFactory(cronetEngine); try { URL.setURLStreamHandlerFactory(cronetURLStreamHandlerFactory); } catch ( Error e) { Log.e(TAG, "Failed to install Cronet URLStreamHandlerFactory"); } urlFactoryInstalled = true; } catch ( Throwable t) { Log.e(TAG, "Failed to install CronetURLStreamHandlerFactory - other"); } } if (doSetupRestarting) { doSetupRestarting = false; } BackgroundUpdateChecker.onMainActivityCreate(this); super.onCreate(savedInstanceState); if (!MainApplication.getSharedPreferences().getBoolean("first_time_user", true)) { this.setActionBarExtraMenuButton(R.drawable.ic_baseline_settings_24, v -> { IntentHelper.startActivity(this, SettingsActivity.class); return true; }, R.string.pref_category_settings); } setContentView(R.layout.activity_main); this.setTitle(R.string.app_name); this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, 0); setActionBarBackground(null); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { WindowManager.LayoutParams layoutParams = this.getWindow().getAttributes(); layoutParams.layoutInDisplayCutoutMode = // Support cutout in Android 9 WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; this.getWindow().setAttributes(layoutParams); } this.actionBarPadding = findViewById(R.id.action_bar_padding); this.actionBarBlur = findViewById(R.id.action_bar_blur); this.actionBarBackground = new ColorDrawable(Color.TRANSPARENT); this.progressIndicator = findViewById(R.id.progress_bar); this.swipeRefreshLayout = findViewById(R.id.swipe_refresh); this.swipeRefreshLayoutOrigStartOffset = this.swipeRefreshLayout.getProgressViewStartOffset(); this.swipeRefreshLayoutOrigEndOffset = this.swipeRefreshLayout.getProgressViewEndOffset(); this.swipeRefreshBlocker = Long.MAX_VALUE; this.moduleList = findViewById(R.id.module_list); this.searchCard = findViewById(R.id.search_card); this.searchView = findViewById(R.id.search_bar); this.moduleViewAdapter = new ModuleViewAdapter(); this.moduleList.setAdapter(this.moduleViewAdapter); this.moduleList.setLayoutManager(new LinearLayoutManager(this)); this.moduleList.setItemViewCacheSize(4); // Default is 2 this.swipeRefreshLayout.setOnRefreshListener(this); this.actionBarBlur.setBackground(this.actionBarBackground); BlurUtils.setupBlur(this.actionBarBlur, this, R.id.blur_frame); this.updateBlurState(); checkShowInitialSetup(); this.moduleList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { if (newState != RecyclerView.SCROLL_STATE_IDLE) MainActivity.this.searchView.clearFocus(); } }); this.searchCard.setRadius(this.searchCard.getHeight() / 2F); this.searchView.setMinimumHeight(FoxDisplay.dpToPixel(16)); this.searchView.setImeOptions(EditorInfo.IME_ACTION_SEARCH | EditorInfo.IME_FLAG_NO_FULLSCREEN); this.searchView.setOnQueryTextListener(this); this.searchView.setOnCloseListener(this); this.searchView.setOnQueryTextFocusChangeListener((v, h) -> { if (!h) { String query = this.searchView.getQuery().toString(); if (query.isEmpty()) { this.searchView.setIconified(true); } } this.cardIconifyUpdate(); }); this.searchView.setEnabled(false); // Enabled later this.cardIconifyUpdate(); this.updateScreenInsets(this.getResources().getConfiguration()); InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() { @Override public void onPathReceived(String path) { Log.i(TAG, "Got magisk path: " + path); if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND) moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED); if (!MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); ModuleManager.getINSTANCE().scan(); ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules); this.commonNext(); } @Override public void onFailure(int error) { Log.i(TAG, "Failed to get magisk path!"); moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification()); this.commonNext(); } public void commonNext() { if (BuildConfig.DEBUG) { Log.d(TAG, "Common next"); moduleViewListBuilder.addNotification(NotificationType.DEBUG); } updateScreenInsets(); // Fix an edge case if (waitInitialSetupFinished()) { if (BuildConfig.DEBUG) { Log.d(TAG, "Initial setup not finished, waiting..."); } return; } swipeRefreshBlocker = System.currentTimeMillis() + 5_000L; if (MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE); if (!Http.hasWebView()) // Check Http for WebView availability moduleViewListBuilder.addNotification(NotificationType.NO_WEB_VIEW); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); runOnUiThread(() -> { progressIndicator.setIndeterminate(false); progressIndicator.setMax(PRECISION); // Fix insets not being accounted for correctly updateScreenInsets(getResources().getConfiguration()); }); // On every preferences change, log the change if debug is enabled if (BuildConfig.DEBUG) { Log.d("PrefsListener", "onCreate: Preferences: " + MainApplication.getSharedPreferences().getAll()); // Log all preferences changes MainApplication.getSharedPreferences().registerOnSharedPreferenceChangeListener((prefs, key) -> Log.i("PrefsListener", "onSharedPreferenceChanged: " + key + " = " + prefs.getAll().get(key))); } Log.i(TAG, "Scanning for modules!"); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Initialize Update"); final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount(); if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) { Log.w(TAG, "Need update on create?"); } if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check Update Compat"); AppUpdateManager.getAppUpdateManager().checkUpdateCompat(); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check Update"); RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); // Add debug notification for debug builds if (!NotificationType.DEBUG.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.DEBUG); } if (!NotificationType.NO_INTERNET.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); } else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED); } else { // Compatibility data still needs to be updated AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager(); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check App Update"); if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true)) moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check Json Update"); if (max != 0) { int current = 0; // noodleDebug.push(""); for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { if (localModuleInfo.updateJson != null) { if (BuildConfig.DEBUG) Log.i("NoodleDebug", localModuleInfo.id); try { localModuleInfo.checkModuleUpdate(); } catch ( Exception e) { Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e); } current++; final int currentTmp = current; runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true)); } } } } runOnUiThread(() -> { progressIndicator.setProgressCompat(PRECISION, true); progressIndicator.setVisibility(View.GONE); searchView.setEnabled(true); setActionBarBackground(null); updateScreenInsets(getResources().getConfiguration()); }); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Apply"); RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); Log.i(TAG, "Finished app opening state!"); // noodleDebug.unbind(); } }, true); ExternalHelper.INSTANCE.refreshHelper(this); this.initMode = false; // Show an material alert dialog if lastEventId is not "" or null in the private sentry shared preferences //noinspection ConstantConditions if (MainApplication.isCrashReportingEnabled() && !BuildConfig.SENTRY_TOKEN.isEmpty()) { SharedPreferences preferences = getSharedPreferences("sentry", MODE_PRIVATE); String lastExitReason = preferences.getString("lastExitReason", ""); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Last Exit Reason: " + lastExitReason); if (lastExitReason.equals("crash")) { String lastEventId = preferences.getString("lastEventId", ""); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Last Event ID: " + lastEventId); if (!lastEventId.equals("")) { // Three edit texts for the user to enter their email, name and a description of the issue EditText email = new EditText(this); email.setHint(R.string.email); email.setInputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); EditText name = new EditText(this); name.setHint(R.string.name); name.setInputType(InputType.TYPE_TEXT_VARIATION_PERSON_NAME); EditText description = new EditText(this); description.setHint(R.string.additional_info); // Set description to be multiline and auto resize description.setSingleLine(false); description.setMaxHeight(1000); // Make description required- new MaterialAlertDialogBuilder(this).setCancelable(false).setTitle(R.string.sentry_dialogue_title).setMessage(R.string.sentry_dialogue_message).setView(new LinearLayout(this) {{ setOrientation(LinearLayout.VERTICAL); setPadding(40, 20, 40, 10); addView(email); addView(name); addView(description); }}).setPositiveButton(R.string.submit, (dialog, which) -> { // Make sure the user has entered a description if (description.getText().toString().equals("")) { Toast.makeText(this, R.string.sentry_dialogue_no_description, Toast.LENGTH_LONG).show(); dialog.cancel(); } preferences.edit().remove("lastEventId").apply(); preferences.edit().putString("lastExitReason", "").apply(); // Prevent strict mode violation new Thread(() -> { try { HttpURLConnection connection = (HttpURLConnection) new URL("https" + "://sentry.io/api/0/projects/androidacy-i6/foxmmm/user-feedback/").openConnection(); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestProperty("Authorization", "Bearer " + BuildConfig.SENTRY_TOKEN); // Setups the JSON body String nameString = name.getText().toString(); String emailString = email.getText().toString(); if (nameString.equals("")) nameString = "Anonymous"; if (emailString.equals("")) emailString = "Anonymous"; JSONObject body = new JSONObject(); body.put("event_id", lastEventId); body.put("name", nameString); body.put("email", emailString); body.put("comments", description.getText().toString()); // Send the request connection.setDoOutput(true); connection.getOutputStream().write(body.toString().getBytes()); connection.connect(); // For debug builds, log the response code and response body if (BuildConfig.DEBUG) { Log.d("NoodleDebug", "Response Code: " + connection.getResponseCode()); } // Check if the request was successful if (connection.getResponseCode() == 200) { runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_success, Toast.LENGTH_LONG).show()); } else { runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show()); } } catch ( IOException | JSONException ignored) { // Show a toast if the user feedback could not be submitted runOnUiThread(() -> Toast.makeText(this, R.string.sentry_dialogue_failed_toast, Toast.LENGTH_LONG).show()); } }).start(); }).setNegativeButton(R.string.cancel, (dialog, which) -> { preferences.edit().remove("lastEventId").apply(); preferences.edit().putString("lastExitReason", "").apply(); Log.w(TAG, "User cancelled sentry dialog"); }).show(); } } } } private void cardIconifyUpdate() { boolean iconified = this.searchView.isIconified(); int backgroundAttr = iconified ? MainApplication.isMonetEnabled() ? R.attr.colorSecondaryContainer : // Monet is special... R.attr.colorSecondary : R.attr.colorPrimarySurface; Resources.Theme theme = this.searchCard.getContext().getTheme(); TypedValue value = new TypedValue(); theme.resolveAttribute(backgroundAttr, value, true); this.searchCard.setCardBackgroundColor(value.data); this.searchCard.setAlpha(iconified ? 0.80F : 1F); } private void updateScreenInsets() { this.runOnUiThread(() -> this.updateScreenInsets(this.getResources().getConfiguration())); } private void updateScreenInsets(Configuration configuration) { boolean landscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE; int bottomInset = (landscape ? 0 : this.getNavigationBarHeight()); int statusBarHeight = getStatusBarHeight(); int actionBarHeight = getActionBarHeight(); int combinedBarsHeight = statusBarHeight + actionBarHeight; this.actionBarPadding.setMinHeight(combinedBarsHeight); this.swipeRefreshLayout.setProgressViewOffset(false, swipeRefreshLayoutOrigStartOffset + combinedBarsHeight, swipeRefreshLayoutOrigEndOffset + combinedBarsHeight); this.moduleViewListBuilder.setHeaderPx(Math.max(statusBarHeight, combinedBarsHeight - FoxDisplay.dpToPixel(4))); this.moduleViewListBuilder.setFooterPx(FoxDisplay.dpToPixel(4) + bottomInset + this.searchCard.getHeight()); this.searchCard.setRadius(this.searchCard.getHeight() / 2F); this.moduleViewListBuilder.updateInsets(); //this.actionBarBlur.invalidate(); this.overScrollInsetTop = combinedBarsHeight; this.overScrollInsetBottom = bottomInset; Log.i(TAG, "( " + bottomInset + ", " + this.searchCard.getHeight() + ")"); } private void updateBlurState() { boolean isLightMode = this.isLightTheme(); int colorBackground; try { colorBackground = this.getColorCompat(android.R.attr.windowBackground); } catch ( Resources.NotFoundException e) { colorBackground = this.getColorCompat(isLightMode ? R.color.white : R.color.black); } if (MainApplication.isBlurEnabled()) { this.actionBarBlur.setBlurEnabled(true); this.actionBarBackground.setColor(ColorUtils.setAlphaComponent(colorBackground, 0x02)); this.actionBarBackground.setColor(Color.TRANSPARENT); } else { this.actionBarBlur.setBlurEnabled(false); this.actionBarBlur.setOverlayColor(Color.TRANSPARENT); this.actionBarBackground.setColor(colorBackground); } } @Override public void refreshUI() { super.refreshUI(); if (this.initMode) return; this.initMode = true; Log.i(TAG, "Item Before"); this.searchView.setQuery("", false); this.searchView.clearFocus(); this.searchView.setIconified(true); this.cardIconifyUpdate(); this.updateScreenInsets(); this.updateBlurState(); this.moduleViewListBuilder.setQuery(null); Log.i(TAG, "Item After"); this.moduleViewListBuilder.refreshNotificationsUI(this.moduleViewAdapter); InstallerInitializer.tryGetMagiskPathAsync(new InstallerInitializer.Callback() { @Override public void onPathReceived(String path) { checkShowInitialSetup(); // Wait for doSetupNow to finish while (doSetupNowRunning) { try { //noinspection BusyWait Thread.sleep(100); } catch ( InterruptedException ignored) { } } if (InstallerInitializer.peekMagiskVersion() < Constants.MAGISK_VER_CODE_INSTALL_COMMAND) moduleViewListBuilder.addNotification(NotificationType.MAGISK_OUTDATED); if (!MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.INSTALL_FROM_STORAGE); ModuleManager.getINSTANCE().scan(); ModuleManager.getINSTANCE().runAfterScan(moduleViewListBuilder::appendInstalledModules); this.commonNext(); } @Override public void onFailure(int error) { moduleViewListBuilder.addNotification(InstallerInitializer.getErrorNotification()); this.commonNext(); } public void commonNext() { Log.i(TAG, "Common Before"); if (MainApplication.isShowcaseMode()) moduleViewListBuilder.addNotification(NotificationType.SHOWCASE_MODE); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); if (!NotificationType.NO_INTERNET.shouldRemove()) moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); else if (AppUpdateManager.getAppUpdateManager().checkUpdate(false)) moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); RepoManager.getINSTANCE().updateEnabledStates(); if (RepoManager.getINSTANCE().getCustomRepoManager().needUpdate()) { runOnUiThread(() -> { progressIndicator.setIndeterminate(false); progressIndicator.setMax(PRECISION); }); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check Update"); RepoManager.getINSTANCE().update(value -> runOnUiThread(() -> progressIndicator.setProgressCompat((int) (value * PRECISION), true))); runOnUiThread(() -> { progressIndicator.setProgressCompat(PRECISION, true); progressIndicator.setVisibility(View.GONE); }); } if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Apply"); RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); Log.i(TAG, "Common Before applyTo"); moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); Log.i(TAG, "Common After"); } }); this.initMode = false; } @Override protected void onWindowUpdated() { this.updateScreenInsets(); } @Override public void onRefresh() { if (this.swipeRefreshBlocker > System.currentTimeMillis() || this.initMode || this.progressIndicator == null || this.progressIndicator.getVisibility() == View.VISIBLE || doSetupNowRunning) { this.swipeRefreshLayout.setRefreshing(false); return; // Do not double scan } if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Refresh"); this.progressIndicator.setVisibility(View.VISIBLE); this.progressIndicator.setProgressCompat(0, false); this.swipeRefreshBlocker = System.currentTimeMillis() + 5_000L; // this.swipeRefreshLayout.setRefreshing(true); ?? new Thread(() -> { Http.cleanDnsCache(); // Allow DNS reload from network // noodleDebug.push("Check Update"); final int max = ModuleManager.getINSTANCE().getUpdatableModuleCount(); RepoManager.getINSTANCE().update(value -> runOnUiThread(max == 0 ? () -> progressIndicator.setProgressCompat((int) (value * PRECISION), true) : () -> progressIndicator.setProgressCompat((int) (value * PRECISION * 0.75F), true))); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); if (!NotificationType.NO_INTERNET.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.NO_INTERNET); } else if (!NotificationType.REPO_UPDATE_FAILED.shouldRemove()) { moduleViewListBuilder.addNotification(NotificationType.REPO_UPDATE_FAILED); } else { // Compatibility data still needs to be updated AppUpdateManager appUpdateManager = AppUpdateManager.getAppUpdateManager(); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check App Update"); if (BuildConfig.ENABLE_AUTO_UPDATER && appUpdateManager.checkUpdate(true)) moduleViewListBuilder.addNotification(NotificationType.UPDATE_AVAILABLE); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Check Json Update"); if (max != 0) { int current = 0; for (LocalModuleInfo localModuleInfo : ModuleManager.getINSTANCE().getModules().values()) { if (localModuleInfo.updateJson != null) { if (BuildConfig.DEBUG) Log.i("NoodleDebug", localModuleInfo.id); try { localModuleInfo.checkModuleUpdate(); } catch ( Exception e) { Log.e("MainActivity", "Failed to fetch update of: " + localModuleInfo.id, e); } current++; final int currentTmp = current; runOnUiThread(() -> progressIndicator.setProgressCompat((int) ((1F * currentTmp / max) * PRECISION * 0.25F + (PRECISION * 0.75F)), true)); } } } } if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Apply"); runOnUiThread(() -> { this.progressIndicator.setVisibility(View.GONE); this.swipeRefreshLayout.setRefreshing(false); }); NotificationType.NEED_CAPTCHA_ANDROIDACY.autoAdd(moduleViewListBuilder); RepoManager.getINSTANCE().updateEnabledStates(); RepoManager.getINSTANCE().runAfterUpdate(moduleViewListBuilder::appendRemoteModules); this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter); /* noodleDebug.unbind(); */ }, "Repo update thread").start(); } @Override public boolean onQueryTextSubmit(final String query) { this.searchView.clearFocus(); if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(query)) { new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return true; } @Override public boolean onQueryTextChange(String query) { if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(query)) { new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return false; } @Override public boolean onClose() { if (this.initMode) return false; if (this.moduleViewListBuilder.setQueryChange(null)) { new Thread(() -> this.moduleViewListBuilder.applyTo(moduleList, moduleViewAdapter), "Query update thread").start(); } return false; } @Override public int getOverScrollInsetTop() { return this.overScrollInsetTop; } @Override public int getOverScrollInsetBottom() { return this.overScrollInsetBottom; } @SuppressLint("RestrictedApi") private void ensurePermissions() { if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Ensure Permissions"); // First, check if user has said don't ask again by checking if pref_dont_ask_again_notification_permission is true if (!PreferenceManager.getDefaultSharedPreferences(this).getBoolean("pref_dont_ask_again_notification_permission", false)) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Request Notification Permission"); if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { // Show a dialog explaining why we need this permission, which is to show // notifications for updates runOnUiThread(() -> { if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Show Notification Permission Dialog"); MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.permission_notification_title); builder.setMessage(R.string.permission_notification_message); // Don't ask again checkbox View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null); CheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.dont_ask_again); checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply()); builder.setView(view); builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> { // Request the permission this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0); doSetupNowRunning = false; }); builder.setNegativeButton(R.string.cancel, (dialog, which) -> { // Set pref_background_update_check to false and dismiss dialog SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.edit().putBoolean("pref_background_update_check", false).apply(); dialog.dismiss(); doSetupNowRunning = false; }); builder.show(); if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Show Notification Permission Dialog Done"); }); } else { // Request the permission if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Request Notification Permission"); this.requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0); if (BuildConfig.DEBUG) { // Log if granted via onRequestPermissionsResult Log.i("NoodleDebug", "Request Notification Permission Done. Result: " + (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED)); } doSetupNowRunning = false; } // Next branch is for < android 13 and user has blocked notifications } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && !NotificationManagerCompat.from(this).areNotificationsEnabled()) { runOnUiThread(() -> { MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this); builder.setTitle(R.string.permission_notification_title); builder.setMessage(R.string.permission_notification_message); // Don't ask again checkbox View view = getLayoutInflater().inflate(R.layout.dialog_checkbox, null); CheckBox checkBox = view.findViewById(R.id.checkbox); checkBox.setText(R.string.dont_ask_again); checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> PreferenceManager.getDefaultSharedPreferences(this).edit().putBoolean("pref_dont_ask_again_notification_permission", isChecked).apply()); builder.setView(view); builder.setPositiveButton(R.string.permission_notification_grant, (dialog, which) -> { // Open notification settings Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", getPackageName(), null); intent.setData(uri); startActivity(intent); doSetupNowRunning = false; }); builder.setNegativeButton(R.string.cancel, (dialog, which) -> { // Set pref_background_update_check to false and dismiss dialog SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); prefs.edit().putBoolean("pref_background_update_check", false).apply(); dialog.dismiss(); doSetupNowRunning = false; }); builder.show(); }); } else { doSetupNowRunning = false; } } else { if (BuildConfig.DEBUG) Log.i("NoodleDebug", "Notification Permission Already Granted or Don't Ask Again"); doSetupNowRunning = false; } } // Method to show a setup box on first launch @SuppressLint({"InflateParams", "RestrictedApi", "UnspecifiedImmutableFlag", "ApplySharedPref"}) private void checkShowInitialSetup() { if (BuildConfig.DEBUG) Log.i("SetupWizard", "Checking if we need to run setup"); // Check if this is the first launch SharedPreferences prefs = MainApplication.getSharedPreferences(); boolean firstLaunch = prefs.getBoolean("first_time_user", true); if (BuildConfig.DEBUG) Log.i("SetupWizard", "First launch: " + firstLaunch); if (firstLaunch) { doSetupNowRunning = true; // Show setup box. Put the setup_box in the main activity layout View view = getLayoutInflater().inflate(R.layout.setup_box, null); // Make the setup_box linear layout the sole child of the root_container constraint layout setContentView(view); updateScreenInsets(); // Handle action bar. Set it to setup_title and make it visible ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setTitle(R.string.app_name); // Set solid color background actionBar.show(); } ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setChecked(BuildConfig.ENABLE_AUTO_UPDATER); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setChecked(BuildConfig.DEFAULT_ENABLE_CRASH_REPORTING); // Repos are a little harder, as the enabled_repos build config is an arraylist ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("androidacy_repo")); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setChecked(BuildConfig.ENABLED_REPOS.contains("magisk_alt_repo")); // On debug builds, log when a switch is toggled if (BuildConfig.DEBUG) { ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).setOnCheckedChangeListener((buttonView, isChecked) -> Log.i("SetupWizard", "Background Update Check: " + isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).setOnCheckedChangeListener((buttonView, isChecked) -> Log.i("SetupWizard", "Crash Reporting: " + isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Log.i("SetupWizard", "Androidacy Repo: " + isChecked)); ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).setOnCheckedChangeListener((buttonView, isChecked) -> Log.i("SetupWizard", "Magisk Alt Repo: " + isChecked)); } // Setup popup dialogue for the setup_theme_button MaterialButton themeButton = view.findViewById(R.id.setup_theme_button); themeButton.setOnClickListener(v -> { // Create a new popup menu PopupMenu popupMenu = new PopupMenu(this, themeButton); // Inflate the menu popupMenu.getMenuInflater().inflate(R.menu.theme_menu, popupMenu.getMenu()); // if pref_theme is set, check the relevant theme_* menu item, otherwise check the default (theme_system) String prefTheme = prefs.getString("pref_theme", "system"); if (BuildConfig.DEBUG) Log.i("SetupWizard", "pref_theme: " + prefTheme); switch (prefTheme) { case "light": popupMenu.getMenu().findItem(R.id.theme_light).setChecked(true); break; case "dark": popupMenu.getMenu().findItem(R.id.theme_dark).setChecked(true); break; case "system": popupMenu.getMenu().findItem(R.id.theme_system).setChecked(true); break; // Black and transparent_light case "black": popupMenu.getMenu().findItem(R.id.theme_black).setChecked(true); break; case "transparent_light": popupMenu.getMenu().findItem(R.id.theme_transparent_light).setChecked(true); break; } // Set the on click listener popupMenu.setOnMenuItemClickListener(item -> { if (item == null) { return false; } // Make sure it.s an actual item, not the overflow menu. Actual items have an id of theme_* (see theme_menu.xml) // Check if item id contains theme_ and return false if it doesn't String itemId = getResources().getResourceEntryName(item.getItemId()); if (!itemId.contains("theme_")) { return false; } // Save the theme. ID is theme_* so we need to remove the first 6 characters // Possible values are light, dark, system, transparent_light, and black prefs.edit().putString("pref_theme", item.getItemId() == R.id.theme_light ? "light" : item.getItemId() == R.id.theme_dark ? "dark" : item.getItemId() == R.id.theme_system ? "system" : item.getItemId() == R.id.theme_transparent_light ? "transparent_light" : "black").commit(); // Set the theme UiThreadHandler.handler.postDelayed(() -> { MainApplication.getINSTANCE().updateTheme(); FoxActivity.getFoxActivity(this).setThemeRecreate(MainApplication.getINSTANCE().getManagerThemeResId()); }, 1); return true; }); // Show the popup menu popupMenu.show(); }); // Set up the buttons // Cancel button MaterialButton cancelButton = view.findViewById(R.id.setup_cancel); cancelButton.setText(R.string.cancel); cancelButton.setOnClickListener(v -> { // Set first launch to false and restart the activity prefs.edit().putBoolean("first_time_user", false).commit(); finish(); startActivity(getIntent()); }); // Setup button MaterialButton setupButton = view.findViewById(R.id.setup_continue); setupButton.setOnClickListener(v -> { // Set first launch to false // get instance of editor SharedPreferences.Editor editor = prefs.edit(); editor.putBoolean("first_time_user", false); // Set the background update check pref editor.putBoolean("pref_background_update_check", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_background_update_check))).isChecked()); // Set the crash reporting pref editor.putBoolean("pref_crash_reporting", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_crash_reporting))).isChecked()); // Set the repos // first pref_magisk_alt_repo_enabled then pref_androidacy_repo_enabled editor.putBoolean("pref_magisk_alt_repo_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_magisk_alt_repo))).isChecked()); editor.putBoolean("pref_androidacy_repo_enabled", ((MaterialSwitch) Objects.requireNonNull(view.findViewById(R.id.setup_androidacy_repo))).isChecked()); // Commit the changes editor.commit(); // Sleep for 1 second to allow the user to see the changes try { Thread.sleep(500); } catch ( InterruptedException e) { e.printStackTrace(); } // Log the changes if debug if (BuildConfig.DEBUG) { Log.d("SetupWizard", "Background update check: " + prefs.getBoolean("pref_background_update_check", false)); Log.i("SetupWizard", "Crash reporting: " + prefs.getBoolean("pref_crash_reporting", false)); Log.i("SetupWizard", "Magisk Alt Repo: " + prefs.getBoolean("pref_magisk_alt_repo_enabled", false)); Log.i("SetupWizard", "Androidacy Repo: " + prefs.getBoolean("pref_androidacy_repo_enabled", false)); } // Restart the activity doSetupRestarting = true; finish(); startActivity(getIntent()); }); } else { ensurePermissions(); } } /** * @return true if the load workflow must be stopped. */ private boolean waitInitialSetupFinished() { if (BuildConfig.DEBUG) Log.i("SetupWizard", "waitInitialSetupFinished"); if (doSetupNowRunning) updateScreenInsets(); // Fix an edge case try { // Wait for doSetupNow to finish while (doSetupNowRunning) { //noinspection BusyWait Thread.sleep(50); } } catch ( InterruptedException e) { return true; } return doSetupRestarting; } }