mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-19 08:33:56 -06:00
The Permissions entry showed a ✓/✗ read-out and then handed off to Android's App Info page. The
screen we actually built for this — a row per permission with its live state and a Manage button
that stays visible once granted — was only reachable during first-run setup, so an installer who
wanted to review or revoke something on a running panel had to re-pair to see it.
Manage Permissions is now the primary action and opens SetupActivity in review mode. Android's App
Info page stays as the secondary, because notification access and some OEM toggles are only
reachable there.
Review mode exists because three things in SetupActivity assume first-run, and every one of them
had to be exempted or this silently did nothing:
- proceedToNext() goes unconditionally to ProvisioningActivity. Without the exemption the button
an installer was told to press would send a paired, playing screen to the pairing page.
- onCreate returns early when setup_complete is set — and every device that can reach this menu
has it set, so the screen closed before it drew and the menu entry looked broken.
- updateStatuses() re-labels the continue button on every refresh, silently overwriting the label
set in onCreate. The label had to move to where it actually sticks.
Review mode also hides the first-run skip hint, does not re-stamp setup_complete, and returns to
playback rather than continuing anywhere.
Verified on an Android 12 emulator, both directions:
in service BACK x2 -> PIN -> Settings -> Permissions -> MANAGE PERMISSIONS -> our screen with
every row and its state -> DONE -> back to playback, no ProvisioningActivity launch,
widget rendering resumed
first run full uninstall + fresh install -> SetupActivity, button reads CONTINUE ANYWAY, skip
hint present, no DONE label, continue lands on ProvisioningActivity, pairing completes
and playback starts
That second run is the one that mattered: both early-exit guards are inverted conditions, and a
mistake in either would have broken onboarding for every new install.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
437 lines
22 KiB
Kotlin
437 lines
22 KiB
Kotlin
package com.remotedisplay.player
|
|
|
|
import android.Manifest
|
|
import android.accessibilityservice.AccessibilityServiceInfo
|
|
import android.annotation.SuppressLint
|
|
import android.app.NotificationManager
|
|
import android.content.ComponentName
|
|
import android.content.Context
|
|
import android.content.Intent
|
|
import android.content.pm.PackageManager
|
|
import android.net.Uri
|
|
import android.os.Build
|
|
import android.os.PowerManager
|
|
import android.os.Bundle
|
|
import android.provider.Settings
|
|
import android.view.View
|
|
import android.view.WindowManager
|
|
import android.view.accessibility.AccessibilityManager
|
|
import android.widget.Button
|
|
import android.widget.TextView
|
|
import androidx.appcompat.app.AppCompatActivity
|
|
import androidx.core.app.ActivityCompat
|
|
import androidx.core.content.ContextCompat
|
|
import com.remotedisplay.player.service.PowerAccessibilityService
|
|
|
|
class SetupActivity : AppCompatActivity() {
|
|
|
|
private lateinit var accessibilityStatus: TextView
|
|
private lateinit var installStatus: TextView
|
|
private lateinit var notificationStatus: TextView
|
|
private lateinit var enableAccessibilityBtn: Button
|
|
private lateinit var enableInstallBtn: Button
|
|
private lateinit var fullscreenStatus: TextView
|
|
private lateinit var enableFullscreenBtn: Button
|
|
private lateinit var batteryStatus: TextView
|
|
private lateinit var enableBatteryBtn: Button
|
|
private lateinit var overlayStatus: TextView
|
|
private lateinit var enableOverlayBtn: Button
|
|
private lateinit var writeSettingsStatus: TextView
|
|
private lateinit var enableWriteSettingsBtn: Button
|
|
private lateinit var continueBtn: Button
|
|
|
|
/**
|
|
* Opened from the in-service Settings menu to REVIEW permissions, not as first-run setup.
|
|
*
|
|
* The difference matters: proceedToNext() always goes to ProvisioningActivity, so without this
|
|
* a paired, playing screen would be sent to the pairing page by the button it was told to press.
|
|
* In manage mode the screen simply returns to the player.
|
|
*/
|
|
private val manageOnly: Boolean get() = intent?.getBooleanExtra(EXTRA_MANAGE_ONLY, false) == true
|
|
|
|
companion object {
|
|
const val EXTRA_MANAGE_ONLY = "EXTRA_MANAGE_ONLY"
|
|
}
|
|
|
|
@SuppressLint("BatteryLife")
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
super.onCreate(savedInstanceState)
|
|
|
|
// Skip setup if already completed — but NOT when we were opened deliberately to review
|
|
// permissions from the in-service Settings menu. That is the whole point of manage mode:
|
|
// every device that can reach it has setup_complete set, so without this exemption the
|
|
// screen closes before it draws and the menu entry appears to do nothing.
|
|
val prefs = getSharedPreferences("remote_display", MODE_PRIVATE)
|
|
if (!manageOnly && prefs.getBoolean("setup_complete", false)) {
|
|
proceedToNext()
|
|
return
|
|
}
|
|
|
|
// #device-owner: a device owner self-configures the kiosk essentials (HOME launcher, kiosk
|
|
// lock-task, notifications) with no user taps, and silent-install makes "unknown sources"
|
|
// moot — so skip the entire manual first-run wizard. Accessibility stays optional (it can't
|
|
// be auto-enabled). Guarded on ownership, so a NORMAL install still gets the full wizard.
|
|
val ownerPolicy = com.remotedisplay.player.admin.STPolicy(this)
|
|
if (!manageOnly && ownerPolicy.isDeviceOwner()) {
|
|
ownerPolicy.applyOnboardingPolicy()
|
|
prefs.edit().putBoolean("setup_complete", true).apply()
|
|
// Remote control needs the accessibility service, and it's the one thing no policy can
|
|
// enable — so on an MDM deploy, route the installer through the guided enable screen when
|
|
// it's still off (it auto-advances once on). Already on -> straight to pairing.
|
|
if (isAccessibilityEnabled()) {
|
|
proceedToNext()
|
|
} else {
|
|
startActivity(Intent(this, OwnerAccessibilityActivity::class.java))
|
|
finish()
|
|
}
|
|
return
|
|
}
|
|
|
|
setContentView(R.layout.activity_setup)
|
|
|
|
// App's UI is up — clear the boot "Starting display…" notification.
|
|
getSystemService(NotificationManager::class.java)?.cancel(999)
|
|
|
|
@Suppress("DEPRECATION")
|
|
window.decorView.systemUiVisibility = (
|
|
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY or
|
|
View.SYSTEM_UI_FLAG_FULLSCREEN or
|
|
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or
|
|
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
|
|
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
|
|
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
)
|
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
|
|
accessibilityStatus = findViewById(R.id.accessibilityStatus)
|
|
installStatus = findViewById(R.id.installStatus)
|
|
notificationStatus = findViewById(R.id.notificationStatus)
|
|
enableAccessibilityBtn = findViewById(R.id.enableAccessibilityBtn)
|
|
enableInstallBtn = findViewById(R.id.enableInstallBtn)
|
|
continueBtn = findViewById(R.id.continueBtn)
|
|
|
|
// Show notification row on Android 13+
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
findViewById<View>(R.id.notificationRow).visibility = View.VISIBLE
|
|
findViewById<Button>(R.id.enableNotificationBtn).setOnClickListener {
|
|
val granted = ContextCompat.checkSelfPermission(
|
|
this, Manifest.permission.POST_NOTIFICATIONS
|
|
) == PackageManager.PERMISSION_GRANTED
|
|
if (granted) {
|
|
// requestPermissions() does nothing once the answer is already given, so it
|
|
// cannot be the way back. App notification settings can toggle it either way.
|
|
try {
|
|
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
|
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
|
})
|
|
} catch (_: Exception) { openAppSettings() }
|
|
} else {
|
|
ActivityCompat.requestPermissions(
|
|
this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
enableAccessibilityBtn.setOnClickListener {
|
|
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
|
}
|
|
|
|
enableInstallBtn.setOnClickListener {
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
}
|
|
}
|
|
|
|
fullscreenStatus = findViewById(R.id.fullscreenStatus)
|
|
enableFullscreenBtn = findViewById(R.id.enableFullscreenBtn)
|
|
batteryStatus = findViewById(R.id.batteryStatus)
|
|
enableBatteryBtn = findViewById(R.id.enableBatteryBtn)
|
|
overlayStatus = findViewById(R.id.overlayStatus)
|
|
enableOverlayBtn = findViewById(R.id.enableOverlayBtn)
|
|
|
|
// Display-over-other-apps: alternate boot-launch path. With this granted the
|
|
// boot receiver can directly start the activity from the background, which
|
|
// works where you can't set a launcher (e.g. Android TV).
|
|
enableOverlayBtn.setOnClickListener {
|
|
startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
}
|
|
|
|
// #160 Track-A: WRITE_SETTINGS — one-time grant that unlocks remote system-brightness +
|
|
// screen-off-timeout control. Optional; media volume + per-window brightness need no grant.
|
|
writeSettingsStatus = findViewById(R.id.writeSettingsStatus)
|
|
enableWriteSettingsBtn = findViewById(R.id.enableWriteSettingsBtn)
|
|
enableWriteSettingsBtn.setOnClickListener {
|
|
try {
|
|
startActivity(Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
} catch (e: Exception) { startActivity(Intent(Settings.ACTION_SETTINGS)) }
|
|
}
|
|
|
|
// Default launcher / HOME: a kiosk MUST be the default launcher, else Android returns to the
|
|
// stock launcher and tears down + recreates the player on a loop (it never renders). Request
|
|
// the HOME role (clean system dialog on API 29+); fall back to the Home-app picker in Settings.
|
|
// OPTIONAL: location, solely so the device page can show the Wi-Fi network name. Requested
|
|
// only when someone taps this row — never at startup, and nothing else in the player depends
|
|
// on it. Once granted (or permanently denied) requestPermissions() stops prompting, so an
|
|
// already-answered row sends you to app settings where it can be changed either way.
|
|
findViewById<Button>(R.id.enableLocationBtn).setOnClickListener {
|
|
if (hasLocationPermission()) openAppSettings()
|
|
else ActivityCompat.requestPermissions(
|
|
this,
|
|
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION),
|
|
101
|
|
)
|
|
}
|
|
findViewById<Button>(R.id.enableLauncherBtn).setOnClickListener { promptSetDefaultLauncher() }
|
|
|
|
// Launch-on-boot needs USE_FULL_SCREEN_INTENT, which Android 14+ auto-revokes
|
|
// for non-calling apps — so the boot full-screen launcher silently fails until
|
|
// the user grants it. Older versions auto-grant it, so only show the row where
|
|
// it can actually be off.
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
// USE_FULL_SCREEN_INTENT is auto-granted before Android 14 — hide the row.
|
|
findViewById<View>(R.id.fullscreenRow).visibility = View.GONE
|
|
} else {
|
|
enableFullscreenBtn.setOnClickListener {
|
|
try {
|
|
startActivity(Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
} catch (e: Exception) {
|
|
startActivity(Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
|
putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Battery-optimization exemption keeps the boot receiver from being deferred
|
|
// and the app from being killed in standby (esp. on OEM / TV boxes).
|
|
enableBatteryBtn.setOnClickListener {
|
|
val exempt = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
|
.isIgnoringBatteryOptimizations(packageName)
|
|
if (exempt) {
|
|
// ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS only ASKS to add an exemption — it
|
|
// offers no way to remove one, so it is a dead end for someone already exempt.
|
|
// The system list is where an exemption can actually be turned back off.
|
|
try { startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) }
|
|
catch (_: Exception) { openAppSettings() }
|
|
} else {
|
|
try {
|
|
startActivity(Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
} catch (e: Exception) {
|
|
try { startActivity(Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)) }
|
|
catch (_: Exception) { openAppSettings() }
|
|
}
|
|
}
|
|
}
|
|
|
|
if (manageOnly) {
|
|
// "Continue anyway" and the skip hint are first-run language; here the only action is
|
|
// to go back to what was already playing.
|
|
continueBtn.text = getString(R.string.settings_perm_done)
|
|
findViewById<TextView>(R.id.skipText).visibility = View.GONE
|
|
}
|
|
|
|
continueBtn.setOnClickListener {
|
|
if (!manageOnly) prefs.edit().putBoolean("setup_complete", true).apply()
|
|
proceedToNext()
|
|
}
|
|
|
|
findViewById<TextView>(R.id.skipText).setOnClickListener {
|
|
prefs.edit().putBoolean("setup_complete", true).apply()
|
|
proceedToNext()
|
|
}
|
|
|
|
updateStatuses()
|
|
}
|
|
|
|
override fun onResume() {
|
|
super.onResume()
|
|
updateStatuses()
|
|
}
|
|
|
|
/**
|
|
* Bind a permission row's button. Granted rows used to set the button GONE, which left the
|
|
* choice one-way: every permission on this screen is granted in system Settings and the app
|
|
* cannot revoke any of them itself, so hiding the only route to that screen meant there was no
|
|
* way back. Reported on #234 — "if I make the app as Home launcher but later on want to remove
|
|
* it then how can I do it?".
|
|
*
|
|
* The button now stays put and relabels. Same tap target, same destination; the label is honest
|
|
* that Settings is where the change happens rather than promising we can revoke it ourselves.
|
|
*/
|
|
private fun bindPermissionButton(btn: Button, granted: Boolean, enableLabel: String) {
|
|
btn.visibility = View.VISIBLE
|
|
btn.text = if (granted) "Manage" else enableLabel
|
|
}
|
|
|
|
/** Last-resort destination: this app's own settings page, where everything can be reached. */
|
|
private fun openAppSettings() {
|
|
try {
|
|
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
data = Uri.parse("package:$packageName")
|
|
})
|
|
} catch (_: Exception) { try { startActivity(Intent(Settings.ACTION_SETTINGS)) } catch (_: Exception) {} }
|
|
}
|
|
|
|
private fun updateStatuses() {
|
|
// Accessibility
|
|
val accessibilityEnabled = isAccessibilityEnabled()
|
|
accessibilityStatus.text = if (accessibilityEnabled) "ON" else "OFF"
|
|
accessibilityStatus.setTextColor(
|
|
if (accessibilityEnabled) 0xFF22C55E.toInt() else 0xFFEF4444.toInt()
|
|
)
|
|
bindPermissionButton(enableAccessibilityBtn, accessibilityEnabled, "Enable")
|
|
|
|
// Install unknown apps
|
|
val canInstall = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
packageManager.canRequestPackageInstalls()
|
|
} else true
|
|
installStatus.text = if (canInstall) "ON" else "OFF"
|
|
installStatus.setTextColor(
|
|
if (canInstall) 0xFF22C55E.toInt() else 0xFFEF4444.toInt()
|
|
)
|
|
bindPermissionButton(enableInstallBtn, canInstall, "Enable")
|
|
|
|
// Notifications (Android 13+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
val hasNotif = ContextCompat.checkSelfPermission(
|
|
this, Manifest.permission.POST_NOTIFICATIONS
|
|
) == PackageManager.PERMISSION_GRANTED
|
|
notificationStatus.text = if (hasNotif) "ON" else "OFF"
|
|
notificationStatus.setTextColor(
|
|
if (hasNotif) 0xFF22C55E.toInt() else 0xFFEF4444.toInt()
|
|
)
|
|
bindPermissionButton(findViewById(R.id.enableNotificationBtn), hasNotif, "Enable")
|
|
}
|
|
|
|
// Launch on boot (full-screen intent — only restrictable on Android 14+)
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
val canFsi = getSystemService(NotificationManager::class.java).canUseFullScreenIntent()
|
|
fullscreenStatus.text = if (canFsi) "ON" else "OFF"
|
|
fullscreenStatus.setTextColor(if (canFsi) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
|
|
bindPermissionButton(enableFullscreenBtn, canFsi, "Enable")
|
|
}
|
|
|
|
// Battery optimization exemption
|
|
val ignoringBattery = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
|
.isIgnoringBatteryOptimizations(packageName)
|
|
batteryStatus.text = if (ignoringBattery) "ON" else "OFF"
|
|
batteryStatus.setTextColor(if (ignoringBattery) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
|
|
bindPermissionButton(enableBatteryBtn, ignoringBattery, "Enable")
|
|
|
|
// Display over other apps
|
|
val canOverlay = Settings.canDrawOverlays(this)
|
|
overlayStatus.text = if (canOverlay) "ON" else "OFF"
|
|
overlayStatus.setTextColor(if (canOverlay) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
|
|
bindPermissionButton(enableOverlayBtn, canOverlay, "Enable")
|
|
|
|
// #160 WRITE_SETTINGS (system brightness / screen-off timeout)
|
|
val canWrite = Settings.System.canWrite(this)
|
|
writeSettingsStatus.text = if (canWrite) "ON" else "OFF"
|
|
writeSettingsStatus.setTextColor(if (canWrite) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
|
|
bindPermissionButton(enableWriteSettingsBtn, canWrite, "Enable")
|
|
|
|
// Optional Wi-Fi-name permission
|
|
val hasLoc = hasLocationPermission()
|
|
val locationStatus = findViewById<TextView>(R.id.locationStatus)
|
|
locationStatus.text = if (hasLoc) "ON" else "OFF"
|
|
locationStatus.setTextColor(if (hasLoc) 0xFF22C55E.toInt() else 0xFF64748B.toInt())
|
|
bindPermissionButton(findViewById(R.id.enableLocationBtn), hasLoc, "Enable")
|
|
|
|
// Default launcher (HOME): kiosk foreground stability requires being the default launcher.
|
|
val isDefaultHome = isDefaultLauncher()
|
|
val launcherStatus = findViewById<TextView>(R.id.launcherStatus)
|
|
launcherStatus.text = if (isDefaultHome) "ON" else "OFF"
|
|
launcherStatus.setTextColor(if (isDefaultHome) 0xFF22C55E.toInt() else 0xFFEF4444.toInt())
|
|
bindPermissionButton(findViewById(R.id.enableLauncherBtn), isDefaultHome, "Set")
|
|
|
|
// Update continue button text
|
|
val allGood = accessibilityEnabled && canInstall
|
|
// updateStatuses() runs after onCreate's setup and re-labels this button every time, so the
|
|
// manage-mode label has to be honoured HERE too — setting it once earlier was silently
|
|
// overwritten. In review mode there is nothing to continue TO; the only action is going back.
|
|
continueBtn.text = when {
|
|
manageOnly -> getString(R.string.settings_perm_done)
|
|
allGood -> "Continue to Setup"
|
|
else -> "Continue Anyway"
|
|
}
|
|
}
|
|
|
|
/** Either location permission is enough for the SSID; coarse suffices below Android 10. */
|
|
private fun hasLocationPermission(): Boolean =
|
|
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
|
|
ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
|
|
|
private fun isDefaultLauncher(): Boolean {
|
|
// Ask the SAME authority the action uses. This used to read resolveActivity(MATCH_DEFAULT_ONLY),
|
|
// which can name us when we are merely a HOME candidate rather than the chosen home app — so
|
|
// the row could say ON while the system still had the OEM launcher as home, and the button
|
|
// then opened the "become home" request dialog instead of the picker. Reported on #234:
|
|
// "in the apk I have granted the permission ... BUT in the settings of the tablet it still
|
|
// shows the tablet native launcher as home."
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
try {
|
|
val rm = getSystemService(android.app.role.RoleManager::class.java)
|
|
if (rm != null && rm.isRoleAvailable(android.app.role.RoleManager.ROLE_HOME)) {
|
|
return rm.isRoleHeld(android.app.role.RoleManager.ROLE_HOME)
|
|
}
|
|
} catch (_: Exception) { /* fall through to the pre-Q check */ }
|
|
}
|
|
val ri = packageManager.resolveActivity(
|
|
Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME),
|
|
PackageManager.MATCH_DEFAULT_ONLY
|
|
)
|
|
return ri?.activityInfo?.packageName == packageName
|
|
}
|
|
|
|
private fun promptSetDefaultLauncher() {
|
|
// Android 10+ (Q): request the HOME role — a clean one-tap system dialog.
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
try {
|
|
val rm = getSystemService(android.app.role.RoleManager::class.java)
|
|
if (rm != null && rm.isRoleAvailable(android.app.role.RoleManager.ROLE_HOME) &&
|
|
!rm.isRoleHeld(android.app.role.RoleManager.ROLE_HOME)) {
|
|
startActivityForResult(rm.createRequestRoleIntent(android.app.role.RoleManager.ROLE_HOME), 200)
|
|
return
|
|
}
|
|
} catch (_: Exception) { /* fall through to the settings picker */ }
|
|
}
|
|
// Fallback: open the "Home app" picker in Settings (works on every version / OEM).
|
|
try { startActivity(Intent(Settings.ACTION_HOME_SETTINGS)) }
|
|
catch (_: Exception) { try { startActivity(Intent(Settings.ACTION_SETTINGS)) } catch (_: Exception) {} }
|
|
}
|
|
|
|
private fun isAccessibilityEnabled(): Boolean {
|
|
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
|
val enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
|
val myComponent = ComponentName(this, PowerAccessibilityService::class.java)
|
|
return enabledServices.any {
|
|
it.resolveInfo.serviceInfo.let { si ->
|
|
ComponentName(si.packageName, si.name) == myComponent
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
updateStatuses()
|
|
}
|
|
|
|
private fun proceedToNext() {
|
|
// Reviewing permissions on a live screen must never restart pairing — just go back.
|
|
if (manageOnly) { finish(); return }
|
|
startActivity(Intent(this, ProvisioningActivity::class.java))
|
|
finish()
|
|
}
|
|
}
|