You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
328 lines
15 KiB
Kotlin
328 lines
15 KiB
Kotlin
/*
|
|
* Copyright (c) 2023 to present Androidacy and contributors. Names, logos, icons, and the Androidacy name are all trademarks of Androidacy and may not be used without license. See LICENSE for more information.
|
|
*/
|
|
|
|
package com.fox2code.mmm.utils
|
|
|
|
import android.Manifest
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.Handler
|
|
import android.os.Looper
|
|
import android.provider.Settings
|
|
import android.view.View
|
|
import android.widget.CheckBox
|
|
import android.widget.CompoundButton
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.app.NotificationManagerCompat
|
|
import androidx.core.content.ContextCompat
|
|
import androidx.preference.PreferenceManager
|
|
import com.fox2code.mmm.BuildConfig
|
|
import com.fox2code.mmm.MainActivity
|
|
import com.fox2code.mmm.MainApplication
|
|
import com.fox2code.mmm.R
|
|
import com.fox2code.mmm.SetupActivity
|
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
import com.google.android.material.snackbar.Snackbar
|
|
import com.topjohnwu.superuser.Shell
|
|
import timber.log.Timber
|
|
|
|
@Suppress("UNUSED_PARAMETER")
|
|
class RuntimeUtils {
|
|
enum class RebootMode {
|
|
REBOOT, RECOVERY, BOOTLOADER, EDL
|
|
}
|
|
|
|
@SuppressLint("RestrictedApi")
|
|
private fun ensurePermissions(context: Context, activity: MainActivity) {
|
|
if (MainApplication.forceDebugLogging) Timber.i("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(context)
|
|
.getBoolean("pref_dont_ask_again_notification_permission", false)
|
|
) {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission(
|
|
context, Manifest.permission.POST_NOTIFICATIONS
|
|
) != PackageManager.PERMISSION_GRANTED
|
|
) {
|
|
if (MainApplication.forceDebugLogging) Timber.i("Request Notification Permission")
|
|
if (MainApplication.INSTANCE!!.lastActivity!!
|
|
.shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)
|
|
) {
|
|
// Show a dialog explaining why we need context permission, which is to show
|
|
// notifications for updates
|
|
activity.runOnUiThread {
|
|
if (MainApplication.forceDebugLogging) Timber.i("Show Notification Permission Dialog")
|
|
val builder = MaterialAlertDialogBuilder(context)
|
|
builder.setTitle(R.string.permission_notification_title)
|
|
builder.setMessage(R.string.permission_notification_message)
|
|
// Don't ask again checkbox
|
|
val view: View =
|
|
activity.layoutInflater.inflate(R.layout.dialog_checkbox, null)
|
|
val checkBox = view.findViewById<CheckBox>(R.id.checkbox)
|
|
checkBox.setText(R.string.dont_ask_again)
|
|
checkBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
|
PreferenceManager.getDefaultSharedPreferences(
|
|
context
|
|
).edit().putBoolean(
|
|
"pref_dont_ask_again_notification_permission", isChecked
|
|
).apply()
|
|
}
|
|
builder.setView(view)
|
|
builder.setPositiveButton(R.string.permission_notification_grant) { _, _ ->
|
|
// Request the permission
|
|
activity.requestPermissions(
|
|
arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0
|
|
)
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
builder.setNegativeButton(R.string.cancel) { dialog, _ ->
|
|
// Set pref_background_update_check to false and dismiss dialog
|
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
prefs.edit().putBoolean("pref_background_update_check", false).apply()
|
|
dialog.dismiss()
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
builder.show()
|
|
if (MainApplication.forceDebugLogging) Timber.i("Show Notification Permission Dialog Done")
|
|
}
|
|
} else {
|
|
// Request the permission
|
|
if (MainApplication.forceDebugLogging) Timber.i("Request Notification Permission")
|
|
activity.requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 0)
|
|
if (BuildConfig.DEBUG) {
|
|
// Log if granted via onRequestPermissionsResult
|
|
val granted = ContextCompat.checkSelfPermission(
|
|
context, Manifest.permission.POST_NOTIFICATIONS
|
|
) == PackageManager.PERMISSION_GRANTED
|
|
if (MainApplication.forceDebugLogging) Timber.i("Request Notification Permission Done. Result: %s", granted)
|
|
}
|
|
MainActivity.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(
|
|
context
|
|
).areNotificationsEnabled()
|
|
) {
|
|
activity.runOnUiThread {
|
|
val builder = MaterialAlertDialogBuilder(context)
|
|
builder.setTitle(R.string.permission_notification_title)
|
|
builder.setMessage(R.string.permission_notification_message)
|
|
// Don't ask again checkbox
|
|
val view: View = activity.layoutInflater.inflate(R.layout.dialog_checkbox, null)
|
|
val checkBox = view.findViewById<CheckBox>(R.id.checkbox)
|
|
checkBox.setText(R.string.dont_ask_again)
|
|
checkBox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
|
PreferenceManager.getDefaultSharedPreferences(
|
|
context
|
|
).edit()
|
|
.putBoolean("pref_dont_ask_again_notification_permission", isChecked)
|
|
.apply()
|
|
}
|
|
builder.setView(view)
|
|
builder.setPositiveButton(R.string.permission_notification_grant) { _, _ ->
|
|
// Open notification settings
|
|
val intent = Intent()
|
|
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
|
val uri = Uri.fromParts("package", activity.packageName, null)
|
|
intent.data = uri
|
|
activity.startActivity(intent)
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
builder.setNegativeButton(R.string.cancel) { dialog, _ ->
|
|
// Set pref_background_update_check to false and dismiss dialog
|
|
val prefs = MainApplication.getSharedPreferences("mmm")!!
|
|
prefs.edit().putBoolean("pref_background_update_check", false).apply()
|
|
dialog.dismiss()
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
builder.show()
|
|
}
|
|
} else {
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
} else {
|
|
if (MainApplication.forceDebugLogging) Timber.i("Notification Permission Already Granted or Don't Ask Again")
|
|
MainActivity.doSetupNowRunning = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows setup activity if it's the first launch
|
|
*/
|
|
// Method to show a setup box on first launch
|
|
@SuppressLint("InflateParams", "RestrictedApi", "UnspecifiedImmutableFlag", "ApplySharedPref")
|
|
fun checkShowInitialSetup(context: Context, activity: MainActivity) {
|
|
if (MainApplication.forceDebugLogging) Timber.i("Checking if we need to run setup")
|
|
// Check if context is the first launch using prefs and if doSetupRestarting was passed in the intent
|
|
val prefs = MainApplication.getSharedPreferences("mmm")!!
|
|
var firstLaunch = prefs.getString("last_shown_setup", null) != "v5"
|
|
// First launch
|
|
// context is intentionally separate from the above if statement, because it needs to be checked even if the first launch check is true due to some weird edge cases
|
|
if (activity.intent.getBooleanExtra("doSetupRestarting", false)) {
|
|
// Restarting setup
|
|
firstLaunch = false
|
|
}
|
|
if (BuildConfig.DEBUG) {
|
|
if (MainApplication.forceDebugLogging) Timber.i(
|
|
"First launch: %s, pref value: %s",
|
|
firstLaunch,
|
|
prefs.getString("last_shown_setup", null)
|
|
)
|
|
}
|
|
if (firstLaunch) {
|
|
MainActivity.doSetupNowRunning = true
|
|
// Launch setup wizard
|
|
if (MainApplication.forceDebugLogging) Timber.i("Launching setup wizard")
|
|
// Show setup activity
|
|
val intent = Intent(context, SetupActivity::class.java)
|
|
activity.finish()
|
|
activity.startActivity(intent)
|
|
} else {
|
|
ensurePermissions(context, activity)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return true if the load workflow must be stopped.
|
|
*/
|
|
fun waitInitialSetupFinished(context: Context, activity: MainActivity): Boolean {
|
|
if (MainApplication.forceDebugLogging) Timber.i("waitInitialSetupFinished")
|
|
try {
|
|
// Wait for doSetupNow to finish
|
|
while (MainActivity.doSetupNowRunning) {
|
|
Thread.sleep(50)
|
|
}
|
|
} catch (e: InterruptedException) {
|
|
Thread.currentThread().interrupt()
|
|
return true
|
|
}
|
|
return MainActivity.doSetupRestarting
|
|
}
|
|
|
|
/**
|
|
* Shows a snackbar offering to take users to Weblate if their language is not available.
|
|
*
|
|
* @param language The language code.
|
|
* @param languageName The language name.
|
|
*/
|
|
@SuppressLint("RestrictedApi")
|
|
fun showWeblateSnackbar(
|
|
context: Context, activity: MainActivity, language: String, languageName: String
|
|
) {
|
|
MainActivity.isShowingWeblateSb = true
|
|
// if we haven't shown context snackbar for context version yet
|
|
val prefs = MainApplication.getSharedPreferences("mmm")!!
|
|
if (prefs.getInt("weblate_snackbar_shown", 0) == BuildConfig.VERSION_CODE) return
|
|
val snackbar: Snackbar = Snackbar.make(
|
|
activity.findViewById(R.id.root_container),
|
|
activity.getString(R.string.language_not_available, languageName),
|
|
4000
|
|
)
|
|
snackbar.setAction(R.string.ok) {
|
|
val intent = Intent(Intent.ACTION_VIEW)
|
|
intent.data = Uri.parse("https://translate.nift4.org/engage/foxmmm/?language=$language")
|
|
activity.startActivity(intent)
|
|
}
|
|
snackbar.show()
|
|
// after four seconds, set isShowingWeblateSb to false
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
MainActivity.isShowingWeblateSb = false
|
|
}, 4000)
|
|
prefs.edit().putInt("weblate_snackbar_shown", BuildConfig.VERSION_CODE).apply()
|
|
}
|
|
|
|
/**
|
|
* Shows a snackbar to upgrade androidacy membership.
|
|
* Sure it'll be wildly popular but it's only shown for 7 seconds every 7 days.
|
|
* We could y'know stick ads in the app but we're gonna play nice.
|
|
* @param context
|
|
* @param activity
|
|
*/
|
|
@SuppressLint("RestrictedApi")
|
|
fun showUpgradeSnackbar(context: Context, activity: MainActivity) {
|
|
if (MainApplication.forceDebugLogging) Timber.i("showUpgradeSnackbar start")
|
|
// if sb is already showing, wait 4 seconds and try again
|
|
if (MainActivity.isShowingWeblateSb) {
|
|
Handler(Looper.getMainLooper()).postDelayed({
|
|
showUpgradeSnackbar(context, activity)
|
|
}, 4500)
|
|
return
|
|
}
|
|
val prefs = MainApplication.getSharedPreferences("mmm")!!
|
|
// if last shown < 7 days ago
|
|
if (prefs.getLong("ugsns4", 0) > System.currentTimeMillis() - 604800000) return
|
|
val snackbar: Snackbar = Snackbar.make(
|
|
context,
|
|
activity.findViewById(R.id.blur_frame),
|
|
activity.getString(R.string.upgrade_snackbar),
|
|
7000
|
|
)
|
|
snackbar.setAction(R.string.upgrade_now) {
|
|
val intent = Intent(Intent.ACTION_VIEW)
|
|
intent.data =
|
|
Uri.parse("https://androidacy.com/membership-join/#utm_source=AMMM&utm_medium=app&utm_campaign=upgrade_snackbar")
|
|
activity.startActivity(intent)
|
|
}
|
|
snackbar.setAnchorView(R.id.bottom_navigation)
|
|
snackbar.show()
|
|
// do not show for another 7 days
|
|
prefs.edit().putLong("ugsns4", System.currentTimeMillis()).apply()
|
|
if (MainApplication.forceDebugLogging) Timber.i("showUpgradeSnackbar done")
|
|
}
|
|
|
|
companion object {
|
|
fun reboot(mainActivity: AppCompatActivity, reboot: RebootMode) {
|
|
// reboot based on the reboot cmd from the enum we were passed
|
|
when (reboot) {
|
|
RebootMode.REBOOT -> {
|
|
showRebootDialog(mainActivity, false) {
|
|
Shell.cmd("/system/bin/svc power reboot || /system/bin/reboot").submit()
|
|
}
|
|
}
|
|
|
|
RebootMode.RECOVERY -> {
|
|
// KEYCODE_POWER = 26, hide incorrect "Factory data reset" message
|
|
showRebootDialog(mainActivity, false) {
|
|
Shell.cmd("/system/bin/input keyevent 26").submit()
|
|
}
|
|
}
|
|
|
|
RebootMode.BOOTLOADER -> {
|
|
showRebootDialog(mainActivity, false) {
|
|
Shell.cmd("/system/bin/svc power reboot bootloader || /system/bin/reboot bootloader")
|
|
.submit()
|
|
}
|
|
}
|
|
|
|
RebootMode.EDL -> {
|
|
showRebootDialog(mainActivity, true) {
|
|
Shell.cmd("/system/bin/reboot edl").submit()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun showRebootDialog(
|
|
mainActivity: AppCompatActivity,
|
|
showExtraWarning: Boolean,
|
|
function: () -> Unit
|
|
) {
|
|
val message =
|
|
if (showExtraWarning) R.string.reboot_extra_warning else R.string.install_terminal_reboot_now_message
|
|
val dialog = MaterialAlertDialogBuilder(mainActivity)
|
|
dialog.setTitle(R.string.reboot)
|
|
dialog.setCancelable(false)
|
|
dialog.setMessage(message)
|
|
dialog.setPositiveButton(R.string.reboot) { _, _ ->
|
|
function()
|
|
}
|
|
dialog.setNegativeButton(R.string.cancel) { _, _ -> }
|
|
dialog.create()
|
|
dialog.show()
|
|
}
|
|
}
|
|
} |