Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ import kotlin.random.nextInt
private const val REQUEST_CODE_KEY = "SESSION_COMMIT_ACTIVITY_REQUEST_CODE"
private const val IS_LOADING_KEY = "SESSION_COMMIT_ACTIVITY_IS_LOADING"
private const val IS_CONFIG_CHANGE_RECREATION_KEY = "SESSION_COMMIT_ACTIVITY_IS_CONFIG_CHANGE_RECREATION"
private const val PENDING_WINDOW_FOCUS_ACTION_KEY = "SESSION_COMMIT_ACTIVITY_PENDING_WINDOW_FOCUS_ACTION"
private const val PENDING_WINDOW_FOCUS_ACTION_DELAY_KEY = "SESSION_COMMIT_ACTIVITY_PENDING_WINDOW_FOCUS_ACTION_DELAY"
private const val NO_PENDING_WINDOW_FOCUS_ACTION = 0

@RestrictTo(RestrictTo.Scope.LIBRARY)
internal abstract class SessionCommitActivity<F : Failure> protected constructor(
Expand All @@ -68,6 +71,16 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
private var requestCode = -1
private var isLoading = false
private var isOnActivityResultCalled = false
private var pendingWindowFocusAction = NO_PENDING_WINDOW_FOCUS_ACTION
private var pendingWindowFocusActionDelayMillis = 0L

private val pendingWindowFocusActionRunnable = Runnable {
if (!window.decorView.hasWindowFocus()) {
return@Runnable
}
pendingWindowFocusActionDelayMillis = 0L
runPendingWindowFocusAction()
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -88,12 +101,21 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
override fun onDestroy() {
super.onDestroy()
subscriptions.clear()
handler.removeCallbacks(pendingWindowFocusActionRunnable)
for (callback in handlerCallbacks) {
handler.removeCallbacks(callback)
}
handlerCallbacks.clear()
}

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
logger.debug("onWindowFocusChanged hasFocus=%s", hasFocus)
if (hasFocus) {
runPendingWindowFocusAction()
}
}

@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onBackPressed() {
Expand All @@ -108,6 +130,8 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
outState.putInt(REQUEST_CODE_KEY, requestCode)
outState.putBoolean(IS_CONFIG_CHANGE_RECREATION_KEY, isChangingConfigurations)
outState.putBoolean(IS_LOADING_KEY, isLoading)
outState.putInt(PENDING_WINDOW_FOCUS_ACTION_KEY, pendingWindowFocusAction)
outState.putLong(PENDING_WINDOW_FOCUS_ACTION_DELAY_KEY, pendingWindowFocusActionDelayMillis)
}

final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Expand Down Expand Up @@ -148,6 +172,16 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
}
}

protected fun runOnWindowFocused(action: Int, delayMillis: Long = 0L) {
pendingWindowFocusAction = action
pendingWindowFocusActionDelayMillis = delayMillis
handler.removeCallbacks(pendingWindowFocusActionRunnable)
runPendingWindowFocusAction()
}

protected open fun onWindowFocusAction(action: Int) { // no-op by default
}

protected fun abortSession(message: String? = null) = withCompletableSession { session ->
logger.warn("Aborting session %s from activity with message=%s", ackpineSessionId, message)
session?.complete(
Expand Down Expand Up @@ -183,6 +217,8 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
if (savedInstanceState != null) {
requestCode = savedInstanceState.getInt(REQUEST_CODE_KEY)
isLoading = savedInstanceState.getBoolean(IS_LOADING_KEY)
pendingWindowFocusAction = savedInstanceState.getInt(PENDING_WINDOW_FOCUS_ACTION_KEY)
pendingWindowFocusActionDelayMillis = savedInstanceState.getLong(PENDING_WINDOW_FOCUS_ACTION_DELAY_KEY)
setLoading(isLoading)
val isConfigChangeRecreation = savedInstanceState.getBoolean(IS_CONFIG_CHANGE_RECREATION_KEY)
logger.debug(
Expand All @@ -200,6 +236,30 @@ internal abstract class SessionCommitActivity<F : Failure> protected constructor
}
}

private fun runPendingWindowFocusAction() {
val action = pendingWindowFocusAction
val delayMillis = pendingWindowFocusActionDelayMillis
when {
action == NO_PENDING_WINDOW_FOCUS_ACTION -> logger.debug("No pending window focus action found, ignoring")
!window.decorView.hasWindowFocus() -> logger.debug(
"Window does not have focus, ignoring pending action=%s",
action
)

delayMillis > 0L -> {
logger.debug("Posting window focus action=%s with delay of %sms", action, delayMillis)
handler.removeCallbacks(pendingWindowFocusActionRunnable)
handler.postDelayed(pendingWindowFocusActionRunnable, delayMillis)
}

else -> {
logger.debug("Executing window focus action=%s", action)
pendingWindowFocusAction = NO_PENDING_WINDOW_FOCUS_ACTION
onWindowFocusAction(action)
}
}
}

private fun displayLoading(isLoading: Boolean) {
findViewById<ProgressBar>(R.id.ackpine_session_commit_loading_indicator)?.isVisible = isLoading
if (isLoading) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,9 @@ import android.content.pm.PackageInstaller
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.core.content.ContextCompat
import androidx.core.os.ExecutorCompat
import ru.solrudev.ackpine.Ackpine
import ru.solrudev.ackpine.helpers.concurrent.handleResult
import ru.solrudev.ackpine.helpers.concurrent.map
Expand All @@ -43,6 +40,12 @@ private const val TAG = "SessionBasedInstallConfirmationActivity"
private const val CAN_INSTALL_PACKAGES_KEY = "CAN_INSTALL_PACKAGES"
private const val IS_FIRST_RESUME_KEY = "IS_FIRST_RESUME"
private const val WAS_ON_TOP_ON_START_KEY = "WAS_ON_TOP_ON_START"
private const val IS_ON_ACTIVITY_RESULT_CALLED_KEY = "IS_ON_ACTIVITY_RESULT_CALLED"
private const val PENDING_RESULT_CODE_KEY = "PENDING_RESULT_CODE"
private const val NO_PENDING_RESULT_CODE = Int.MIN_VALUE
private const val ACTION_PROCESS_CONFIRMATION_RESULT = 1
private const val ACTION_CHECK_DISMISSAL = 2
private const val ACTION_DEAD_SESSION_FALLBACK = 3

@RestrictTo(RestrictTo.Scope.LIBRARY)
internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
Expand All @@ -63,31 +66,19 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& intent.getBooleanExtra(PackageInstaller.EXTRA_PRE_APPROVAL, false)

private val handler = Handler(Looper.getMainLooper())
private val executor = ExecutorCompat.create(handler)
private val executor by lazy(LazyThreadSafetyMode.NONE) {
ContextCompat.getMainExecutor(this)
}

private var canInstallPackages = false
private var isFirstResume = true
private var wasOnTopOnStart = false
private var isOnActivityResultCalled = false
private var pendingResultCode = NO_PENDING_RESULT_CODE

private val packageInstaller: PackageInstaller
get() = packageManager.packageInstaller

private val deadSessionCompletionRunnable = Runnable {
isSessionStuck().handleResult(executor) { isSessionStuck ->
if (!isSessionStuck) {
// Session proceeded normally after timeout.
finish()
return@handleResult
}
completeSession(
Session.State.Failed(
InstallFailure.Generic(message = "Session $sessionId is dead.")
)
)
}
}

override fun shouldNotifyWhenCommitted() = !isPreapproval

override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -97,6 +88,8 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
canInstallPackages = savedInstanceState.getBoolean(CAN_INSTALL_PACKAGES_KEY)
isFirstResume = savedInstanceState.getBoolean(IS_FIRST_RESUME_KEY)
wasOnTopOnStart = savedInstanceState.getBoolean(WAS_ON_TOP_ON_START_KEY)
isOnActivityResultCalled = savedInstanceState.getBoolean(IS_ON_ACTIVITY_RESULT_CALLED_KEY)
pendingResultCode = savedInstanceState.getInt(PENDING_RESULT_CODE_KEY, NO_PENDING_RESULT_CODE)
}
if (isPreapproval) {
handlePreapproval(launchConfirmation = isFirstCreate)
Expand All @@ -120,18 +113,8 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
return
}
val isConfirmationDismissed = !isOnActivityResultCalled && wasOnTopOnStart
isSessionStuck().handleResult(executor) { isSessionStuck ->
if (isConfirmationDismissed && isSessionStuck) {
// Activity was recreated and brought to top, but install confirmation from OS was dismissed.
abortSession()
}
}
}

override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
handler.removeCallbacks(deadSessionCompletionRunnable)
if (isConfirmationDismissed) {
runOnWindowFocused(ACTION_CHECK_DISMISSAL)
}
}

Expand All @@ -140,10 +123,32 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
outState.putBoolean(CAN_INSTALL_PACKAGES_KEY, canInstallPackages)
outState.putBoolean(IS_FIRST_RESUME_KEY, isFirstResume)
outState.putBoolean(WAS_ON_TOP_ON_START_KEY, wasOnTopOnStart)
outState.putBoolean(IS_ON_ACTIVITY_RESULT_CALLED_KEY, isOnActivityResultCalled)
outState.putInt(PENDING_RESULT_CODE_KEY, pendingResultCode)
}

override fun onActivityResult(resultCode: Int) {
isOnActivityResultCalled = true
pendingResultCode = resultCode
runOnWindowFocused(ACTION_PROCESS_CONFIRMATION_RESULT)
}

override fun onWindowFocusAction(action: Int) {
when (action) {
ACTION_PROCESS_CONFIRMATION_RESULT -> {
val resultCode = pendingResultCode
if (resultCode != NO_PENDING_RESULT_CODE) {
pendingResultCode = NO_PENDING_RESULT_CODE
processConfirmationResult(resultCode)
}
}

ACTION_CHECK_DISMISSAL -> checkDismissal()
ACTION_DEAD_SESSION_FALLBACK -> processDeadSessionFallback()
}
}

private fun processConfirmationResult(resultCode: Int) {
val isActivityCancelled = resultCode == RESULT_CANCELED
val previousCanInstallPackagesValue = canInstallPackages
canInstallPackages = canInstallPackages()
Expand All @@ -167,6 +172,32 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
}
}

private fun checkDismissal() {
val isConfirmationDismissed = !isOnActivityResultCalled && wasOnTopOnStart
if (!isConfirmationDismissed) {
return
}
isSessionStuck().handleResult(executor) { isSessionStuck ->
if (isSessionStuck) {
// Activity was recreated and brought to top, but install confirmation from OS was dismissed.
abortSession()
}
}
}

private fun processDeadSessionFallback() = isSessionStuck().handleResult(executor) { isSessionStuck ->
if (!isSessionStuck) {
// Session proceeded normally after timeout.
finish()
return@handleResult
}
completeSession(
Session.State.Failed(
InstallFailure.Generic(message = "Session $sessionId is dead.")
)
)
}

private fun onInstallConfirmationFinished(
isSessionStuck: Boolean,
isInstallPermissionStatusChanged: Boolean,
Expand Down Expand Up @@ -207,7 +238,7 @@ internal class SessionBasedInstallConfirmationActivity : InstallActivity(TAG) {
// Wait for possible progress/result from PackageInstallerStatusReceiver before completing with failure.
logger.info("Waiting for delayed dead-session fallback for session %s", ackpineSessionId)
setLoading(isLoading = true, delayMillis = 100)
handler.postDelayed(deadSessionCompletionRunnable, 1000)
runOnWindowFocused(ACTION_DEAD_SESSION_FALLBACK, delayMillis = 1000)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,20 @@ package ru.solrudev.ackpine.impl.uninstaller.activity
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import ru.solrudev.ackpine.Ackpine
import ru.solrudev.ackpine.impl.helpers.getParcelableCompat
import ru.solrudev.ackpine.impl.helpers.isPackageInstalled

private const val TAG = "PackageInstallerBasedUninstallActivity"
private const val ACTION_ABORT_IF_PACKAGE_STILL_INSTALLED = 1

@RestrictTo(RestrictTo.Scope.LIBRARY)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal class PackageInstallerBasedUninstallActivity : UninstallActivity(TAG) {

private val logger = Ackpine.loggerProvider.withTag(TAG)
private val handler = Handler(Looper.getMainLooper())

private val abortedSessionRunnable = Runnable {
val packageName = getUninstalledPackageName()
if (packageName == null) {
logger.error("Missing package name for session %s", ackpineSessionId)
completeSessionExceptionally(IllegalStateException("$TAG: packageName was null."))
finish()
return@Runnable
}
if (isPackageInstalled(packageName)) {
logger.warn("Package installer uninstall appears aborted for session %s", ackpineSessionId)
abortSession("Aborted by user")
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Expand All @@ -57,13 +41,6 @@ internal class PackageInstallerBasedUninstallActivity : UninstallActivity(TAG) {
}
}

override fun onDestroy() {
super.onDestroy()
if (isFinishing) {
handler.removeCallbacks(abortedSessionRunnable)
}
}

override fun processResult(resultCode: Int) {
// Wait for possible result from PackageInstallerStatusReceiver before completing with failure.
logger.debug(
Expand All @@ -72,7 +49,13 @@ internal class PackageInstallerBasedUninstallActivity : UninstallActivity(TAG) {
resultCode
)
setLoading(isLoading = true, delayMillis = 200)
handler.postDelayed(abortedSessionRunnable, 400)
runOnWindowFocused(ACTION_ABORT_IF_PACKAGE_STILL_INSTALLED, delayMillis = 400)
}

override fun onWindowFocusAction(action: Int) {
if (action == ACTION_ABORT_IF_PACKAGE_STILL_INSTALLED) {
abortIfPackageStillInstalled()
}
}

override fun launchUninstallActivity() {
Expand All @@ -86,5 +69,19 @@ internal class PackageInstallerBasedUninstallActivity : UninstallActivity(TAG) {
?.let(::startActivityForResult)
}

private fun abortIfPackageStillInstalled() {
val packageName = getUninstalledPackageName()
if (packageName == null) {
logger.error("Missing package name for session %s", ackpineSessionId)
completeSessionExceptionally(IllegalStateException("$TAG: packageName was null."))
finish()
return
}
if (isPackageInstalled(packageName)) {
logger.warn("Package installer uninstall appears aborted for session %s", ackpineSessionId)
abortSession("Aborted by user")
}
}

private fun getUninstalledPackageName() = intent.getStringExtra(EXTRA_PACKAGE_NAME)
}
Loading
Loading