diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/activity/OnboardingActivity.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/activity/OnboardingActivity.kt index fdb7638be..96976328f 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/activity/OnboardingActivity.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/activity/OnboardingActivity.kt @@ -27,8 +27,6 @@ class OnboardingActivity : BaseActivity(){ binding = ActivityOnboardingBinding.inflate(layoutInflater) setContentView(binding.root) - fragment - supportFragmentManager .beginTransaction() .replace(R.id.onboarding_fragment_container, fragment) @@ -55,9 +53,7 @@ class OnboardingActivity : BaseActivity(){ { val finished = !fragment.onBackPressed() if(finished) - { - this.finishAffinity() - } + finish() return finished } @@ -74,5 +70,6 @@ class OnboardingActivity : BaseActivity(){ { const val EXTRA_FIX_PERMS = "FixPermissions" const val EXTRA_ROOT_SETUP_DUMP_PERM = "RootSetupDumpPerm" + const val EXTRA_ROOTLESS_REDO_ADB_SETUP = "RootlessRedoAdbSetup" } } \ No newline at end of file diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/OnboardingFragment.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/OnboardingFragment.kt index 7234dd259..5344487ed 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/OnboardingFragment.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/OnboardingFragment.kt @@ -23,6 +23,7 @@ import me.timschneeberger.hiddenapi_impl.UserHandle import me.timschneeberger.rootlessjamesdsp.BuildConfig import me.timschneeberger.rootlessjamesdsp.R import me.timschneeberger.rootlessjamesdsp.activity.MainActivity +import me.timschneeberger.rootlessjamesdsp.activity.OnboardingActivity.Companion.EXTRA_ROOTLESS_REDO_ADB_SETUP import me.timschneeberger.rootlessjamesdsp.activity.OnboardingActivity.Companion.EXTRA_ROOT_SETUP_DUMP_PERM import me.timschneeberger.rootlessjamesdsp.databinding.OnboardingFragmentBinding import me.timschneeberger.rootlessjamesdsp.flavor.RootShellImpl @@ -71,6 +72,7 @@ class OnboardingFragment : Fragment() { private lateinit var runtimePermissionLauncher: ActivityResultLauncher> private var useRoot: Boolean = false + private var redoAdbSetup: Boolean = false private var shizukuAlive = false private val prefsApp: Preferences.App by inject() @@ -115,6 +117,7 @@ class OnboardingFragment : Fragment() { bundle: Bundle? ): View { useRoot = requireActivity().intent.getBooleanExtra(EXTRA_ROOT_SETUP_DUMP_PERM, false) + redoAdbSetup = requireActivity().intent.getBooleanExtra(EXTRA_ROOTLESS_REDO_ADB_SETUP, false) binding = OnboardingFragmentBinding.inflate(layoutInflater, viewGroup, false) return binding.root } @@ -127,7 +130,7 @@ class OnboardingFragment : Fragment() { backButton.setOnClickListener { changePage(false) } nextButton.setOnClickListener { changePage(true) } - if(useRoot) { + if(useRoot || redoAdbSetup) { pageMap.remove(PAGE_RUNTIME_PERMISSIONS) pageMap.remove(PAGE_READY) goToPage(PAGE_METHOD_SELECT) @@ -163,7 +166,6 @@ class OnboardingFragment : Fragment() { methodPage.methodsShizukuCard.setCardBackgroundColor( requireContext().resolveColorAttribute(com.google.android.material.R.attr.colorSecondaryContainer) ) - } methodPage.methodsRootCard.setOnClickListener { @@ -333,9 +335,11 @@ class OnboardingFragment : Fragment() { @SuppressLint("ApplySharedPref") private fun finishSetup() { - val intent = context?.let { Intent(it, MainActivity::class.java) } ?: return - intent.putExtra(MainActivity.EXTRA_FORCE_SHOW_CAPTURE_PROMPT, true) - startActivity(intent) + if(!redoAdbSetup) { + val intent = context?.let { Intent(it, MainActivity::class.java) } ?: return + intent.putExtra(MainActivity.EXTRA_FORCE_SHOW_CAPTURE_PROMPT, true) + startActivity(intent) + } activity?.finish() // Mark setup as done @@ -414,8 +418,8 @@ class OnboardingFragment : Fragment() { return } - // Root setup; cut-off first two pages - if(useRoot && !forward && (currentPage - 1) <= PAGE_LIMITATIONS) { + // Root setup or rootless re-setup; cut-off first two pages + if((redoAdbSetup || useRoot) && !forward && (currentPage - 1) <= PAGE_LIMITATIONS) { requireActivity().finish() return } @@ -426,8 +430,9 @@ class OnboardingFragment : Fragment() { private fun requestNextPage(nextPage: Int, forward: Boolean): Int { val shouldSkip = when (nextPage) { - PAGE_METHOD_SELECT -> requireContext().hasDumpPermission() - PAGE_ADB_SETUP -> requireContext().hasDumpPermission() + // Don't skip ADB setup if redoAdbSetup is set + PAGE_METHOD_SELECT -> requireContext().hasDumpPermission() && !redoAdbSetup + PAGE_ADB_SETUP -> requireContext().hasDumpPermission() && !redoAdbSetup PAGE_RUNTIME_PERMISSIONS -> { requireContext().hasNotificationPermission() && requireContext().hasRecordPermission() } @@ -452,8 +457,11 @@ class OnboardingFragment : Fragment() { } private fun ensureDumpPermission(): Boolean{ - // Permission already granted? - if(requireContext().hasDumpPermission()) { + /* Permission already granted? + * Note: If were are redoing the ADB setup, make sure that the Shizuku setup + * can run regardless to grant optional permissions + */ + if(requireContext().hasDumpPermission() && (!redoAdbSetup || selectedSetupMethod != SetupMethods.Shizuku)) { Timber.d("DUMP permission granted") return true } @@ -484,6 +492,22 @@ class OnboardingFragment : Fragment() { Timber.e(ex) } + // Grant permanent SYSTEM_ALERT_WINDOW op as shell + try { + val result = ShizukuSystemServerApi.AppOpsService_setMode( + ShizukuSystemServerApi.APP_OPS_OP_SYSTEM_ALERT_WINDOW, + uid, + pkg, + ShizukuSystemServerApi.APP_OPS_MODE_ALLOW + ) + if(!result) + Timber.e("AppOpsService_setMode for system_alert_window failed") + } + catch (ex: Exception) { + Timber.e("AppOpsService_setMode for system_alert_window threw an exception") + Timber.e(ex) + } + // Grant permanent PROJECT_MEDIA op as shell try { val result = ShizukuSystemServerApi.AppOpsService_setMode( diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/settings/SettingsMiscFragment.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/settings/SettingsMiscFragment.kt index 27152486f..cc217cbf7 100644 --- a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/settings/SettingsMiscFragment.kt +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/fragment/settings/SettingsMiscFragment.kt @@ -1,29 +1,39 @@ package me.timschneeberger.rootlessjamesdsp.fragment.settings +import android.content.Intent import android.os.Bundle +import android.provider.Settings import android.util.Patterns +import androidx.core.content.ContextCompat import androidx.preference.EditTextPreference import androidx.preference.Preference import me.timschneeberger.rootlessjamesdsp.BuildConfig import me.timschneeberger.rootlessjamesdsp.R +import me.timschneeberger.rootlessjamesdsp.activity.OnboardingActivity import me.timschneeberger.rootlessjamesdsp.api.AutoEqClient import me.timschneeberger.rootlessjamesdsp.flavor.CrashlyticsImpl -import me.timschneeberger.rootlessjamesdsp.utils.extensions.AssetManagerExtensions.installPrivateAssets +import me.timschneeberger.rootlessjamesdsp.preference.IconPreference +import me.timschneeberger.rootlessjamesdsp.preference.MaterialSwitchPreference import me.timschneeberger.rootlessjamesdsp.utils.Constants +import me.timschneeberger.rootlessjamesdsp.utils.extensions.AssetManagerExtensions.installPrivateAssets import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.showAlert import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.showYesNoAlert import me.timschneeberger.rootlessjamesdsp.utils.extensions.ContextExtensions.toast +import me.timschneeberger.rootlessjamesdsp.utils.extensions.PermissionExtensions.hasProjectMediaAppOp import me.timschneeberger.rootlessjamesdsp.utils.preferences.Preferences import org.koin.android.ext.android.inject import java.util.Locale class SettingsMiscFragment : SettingsBaseFragment() { - private val autoStartNotify by lazy { findPreference(getString(R.string.key_autostart_prompt_at_boot)) } + private val autoStartNotify by lazy { findPreference(getString(R.string.key_autostart_prompt_at_boot)) } private val repairAssets by lazy { findPreference(getString(R.string.key_troubleshooting_repair_assets)) } private val crashReports by lazy { findPreference(getString(R.string.key_share_crash_reports)) } private val aeqApiUrl by lazy { findPreference(getString(R.string.key_network_autoeq_api_url)) } private val debugDatabase by lazy { findPreference(getString(R.string.key_debug_database)) } + private val permSkipPrompt by lazy { findPreference(getString(R.string.key_misc_permission_skip_prompt)) } + private val permAutoStart by lazy { findPreference(getString(R.string.key_misc_permission_auto_start)) } + private val permRestartSetup by lazy { findPreference(getString(R.string.key_misc_permission_restart_setup)) } private val preferences: Preferences.App by inject() @@ -56,9 +66,7 @@ class SettingsMiscFragment : SettingsBaseFragment() { client.queryProfiles( "conntest", onResponse = { _, _ -> - context?.let { - it.toast(R.string.network_autoeq_conntest_done, false) - } + context?.toast(R.string.network_autoeq_conntest_done, false) }, onFailure = { error -> context?.showYesNoAlert( @@ -83,9 +91,63 @@ class SettingsMiscFragment : SettingsBaseFragment() { true } + permRestartSetup?.setOnPreferenceClickListener { + startActivity(Intent(context, OnboardingActivity::class.java).apply { + putExtra(OnboardingActivity.EXTRA_ROOTLESS_REDO_ADB_SETUP, true) + }) + true + } + + updatePermissionStates() + crashReports?.parent?.isVisible = !BuildConfig.FOSS_ONLY debugDatabase?.parent?.isVisible = BuildConfig.DEBUG autoStartNotify?.isVisible = BuildConfig.ROOTLESS + permRestartSetup?.parent?.isVisible = BuildConfig.ROOTLESS + } + + override fun onResume() { + updatePermissionStates() + super.onResume() + } + + private fun updatePermissionStates() { + if(!BuildConfig.ROOTLESS) + return + + val allowSkipPrompt = context?.hasProjectMediaAppOp() == true + val allowAutoStart = allowSkipPrompt && Settings.canDrawOverlays(context) + + autoStartNotify?.title = getString( + if(allowAutoStart) R.string.autostart_service_at_boot + else R.string.autostart_prompt_at_boot + ) + autoStartNotify?.summaryOn = getString( + if(allowAutoStart) R.string.autostart_service_at_boot_on + else R.string.autostart_prompt_at_boot_on + ) + autoStartNotify?.summaryOff = getString( + if(allowAutoStart) R.string.autostart_service_at_boot_off + else R.string.autostart_prompt_at_boot_off + ) + + fun getIcon(allowed: Boolean) = context?.let { + ContextCompat.getDrawable(it, + if(allowed) R.drawable.ic_twotone_check_circle_24dp + else R.drawable.ic_twotone_warning_24dp + ) + } + + fun getSummary(allowed: Boolean) = context?.getString( + if(allowed) R.string.permission_allowed + else R.string.permission_not_allowed + ) + + permSkipPrompt?.summary = getSummary(allowSkipPrompt) + permAutoStart?.summary = getSummary(allowAutoStart) + permSkipPrompt?.icon = getIcon(allowSkipPrompt) + permAutoStart?.icon = getIcon(allowAutoStart) + permRestartSetup?.isVisible = !allowSkipPrompt || !allowAutoStart } companion object { diff --git a/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/IconPreference.kt b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/IconPreference.kt new file mode 100644 index 000000000..0c78c077b --- /dev/null +++ b/app/src/main/java/me/timschneeberger/rootlessjamesdsp/preference/IconPreference.kt @@ -0,0 +1,21 @@ +package me.timschneeberger.rootlessjamesdsp.preference + +import android.content.Context +import android.util.AttributeSet +import androidx.preference.Preference +import me.timschneeberger.rootlessjamesdsp.R + + +open class IconPreference( + mContext: Context, val attrs: AttributeSet?, + defStyleAttr: Int, defStyleRes: Int, +) : Preference(mContext, attrs, defStyleAttr, defStyleRes) { + + @JvmOverloads + constructor( + context: Context, attrs: AttributeSet? = null, + defStyle: Int = androidx.preference.R.attr.preferenceStyle, + ) : this(context, attrs, defStyle, 0) { + layoutResource = R.layout.preference_icon + } +} diff --git a/app/src/main/res/drawable/ic_numeric_1_circle_outline.xml b/app/src/main/res/drawable/ic_numeric_1_circle_outline.xml index 7c41538e7..7d520e6e2 100644 --- a/app/src/main/res/drawable/ic_numeric_1_circle_outline.xml +++ b/app/src/main/res/drawable/ic_numeric_1_circle_outline.xml @@ -1,6 +1,6 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 80c0c232d..86210c9a5 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -25,6 +25,12 @@ + + + + + + @@ -32,7 +38,7 @@ - + diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 0dd960c93..d7df9c99b 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -66,6 +66,9 @@ credits_google_play audioformat_enhanced_processing_info device_profiles_info + misc_permission_skip_prompt + misc_permission_auto_start + misc_permission_restart_setup editor_font_size diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bb27c8c39..b3071c59c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,8 @@ Undo Redo No Activity found to handle action + Allowed + Not allowed Toggle audio processing @@ -226,7 +228,7 @@ Navigate to \'Interactive Shell\' and execute the following command in the shell: \'pm grant %s android.permission.DUMP\' Tap \'Next\' in this app to continue. (Optional) You can permanently skip the audio capture permission prompts by executing this command: \'appops set %s PROJECT_MEDIA allow\' - (Optional) In addition to the previous step, you can also enable auto-start by executing the following command: \'pm grant %s android.permission.SYSTEM_ALERT_WINDOW\' + (Optional) In addition to the previous step, you can also enable auto-start by executing the following command: \'appops set %s SYSTEM_ALERT_WINDOW allow\' Open development settings Other permissions @@ -310,6 +312,9 @@ Prompt for capture permission after boot Don\'t show notification after boot Show notification after boot + Launch after boot + Don\'t start service automatically after boot + Start service automatically after boot Power-saving Suspend audio pipeline while idle Keep processing even when no content is playing @@ -330,6 +335,11 @@ Bundled assets have been restored Miscellaneous Crash reports, other actions + Skip capture permission prompt + Auto-start effect engine on boot + Restart setup wizard + Grant missing optional permissions + Optional permissions Assets Network services AutoEQ API backend URL diff --git a/app/src/main/res/xml/app_misc_preferences.xml b/app/src/main/res/xml/app_misc_preferences.xml index 254cfd52d..596dbc878 100644 --- a/app/src/main/res/xml/app_misc_preferences.xml +++ b/app/src/main/res/xml/app_misc_preferences.xml @@ -1,5 +1,6 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> + + + + + +