mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-06-07 16:29:55 +00:00
Fix install prompt notification, further automate updates/installs.
Fixes "tap to install" notification not launching an activity. Install notifications moved to InstallerService so that we only show them for apps that need intervention (and lets us preserve our install session). All installs go to the installer, allowing for downloads to jump straight into installation if possible (auto updates or showing prompt on A9 and earlier). If a prompt is needed, the notification is still shown. Additional utility function checks if app is in the foreground. Used to ensure that we do not launch a prompt that cannot be launched. Miscellaneous comments have been added and improved.
This commit is contained in:
parent
ddb7422e45
commit
73ac718ce2
@ -1,9 +1,15 @@
|
|||||||
package com.looker.droidify
|
package com.looker.droidify
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
|
import android.net.Uri
|
||||||
import com.looker.droidify.ContextWrapperX.Companion.wrap
|
import com.looker.droidify.ContextWrapperX.Companion.wrap
|
||||||
|
import com.looker.droidify.installer.InstallerService
|
||||||
import com.looker.droidify.screen.ScreenActivity
|
import com.looker.droidify.screen.ScreenActivity
|
||||||
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class MainActivity : ScreenActivity() {
|
class MainActivity : ScreenActivity() {
|
||||||
companion object {
|
companion object {
|
||||||
@ -16,12 +22,35 @@ class MainActivity : ScreenActivity() {
|
|||||||
override fun handleIntent(intent: Intent?) {
|
override fun handleIntent(intent: Intent?) {
|
||||||
when (intent?.action) {
|
when (intent?.action) {
|
||||||
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
|
ACTION_UPDATES -> handleSpecialIntent(SpecialIntent.Updates)
|
||||||
ACTION_INSTALL -> handleSpecialIntent(
|
ACTION_INSTALL -> {
|
||||||
SpecialIntent.Install(
|
// continue install prompt
|
||||||
intent.packageName,
|
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
|
||||||
)
|
promptIntent?.let {
|
||||||
)
|
it.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
|
||||||
|
it.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, BuildConfig.APPLICATION_ID)
|
||||||
|
it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK )
|
||||||
|
|
||||||
|
startActivity(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: send this back to the InstallerService to free up the UI
|
||||||
|
// prepare prompt intent
|
||||||
|
// val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||||
|
//
|
||||||
|
// val pending = PendingIntent.getService(
|
||||||
|
// this,
|
||||||
|
// 0,
|
||||||
|
// Intent(this, InstallerService::class.java)
|
||||||
|
// .setData(Uri.parse("package:$name"))
|
||||||
|
// .putExtra(Intent.EXTRA_INTENT, promptIntent)
|
||||||
|
// .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
// if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
// else PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// pending.send()
|
||||||
|
}
|
||||||
else -> super.handleIntent(intent)
|
else -> super.handleIntent(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,11 +33,15 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
|||||||
|
|
||||||
override suspend fun install(packageName: String, cacheFileName: String) {
|
override suspend fun install(packageName: String, cacheFileName: String) {
|
||||||
val cacheFile = Cache.getReleaseFile(context, cacheFileName)
|
val cacheFile = Cache.getReleaseFile(context, cacheFileName)
|
||||||
|
// using packageName to store the app's name for the notification later down the line
|
||||||
|
intent.putExtra(InstallerService.KEY_APP_NAME, packageName)
|
||||||
mDefaultInstaller(cacheFile)
|
mDefaultInstaller(cacheFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun install(packageName: String, cacheFile: File) =
|
override suspend fun install(packageName: String, cacheFile: File) {
|
||||||
|
intent.putExtra(InstallerService.KEY_APP_NAME, packageName)
|
||||||
mDefaultInstaller(cacheFile)
|
mDefaultInstaller(cacheFile)
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
|
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
|
||||||
|
|
||||||
|
@ -1,32 +1,41 @@
|
|||||||
package com.looker.droidify.installer
|
package com.looker.droidify.installer
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageInstaller
|
import android.content.pm.PackageInstaller
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.looker.droidify.Common
|
import com.looker.droidify.Common.NOTIFICATION_CHANNEL_DOWNLOADING
|
||||||
|
import com.looker.droidify.Common.NOTIFICATION_ID_DOWNLOADING
|
||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
|
import com.looker.droidify.MainActivity
|
||||||
|
import com.looker.droidify.utility.Utils
|
||||||
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import com.looker.droidify.utility.extension.android.notificationManager
|
import com.looker.droidify.utility.extension.android.notificationManager
|
||||||
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
import com.looker.droidify.utility.extension.resources.getColorFromAttr
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs during or after a PackageInstaller session in order to handle completion, failure, or
|
* Runs during or after a PackageInstaller session in order to handle completion, failure, or
|
||||||
* interruptions requiring user intervention (e.g. "Install Unknown Apps" permission requests).
|
* interruptions requiring user intervention, such as the package installer prompt.
|
||||||
*/
|
*/
|
||||||
class InstallerService : Service() {
|
class InstallerService : Service() {
|
||||||
companion object {
|
companion object {
|
||||||
const val KEY_ACTION = "installerAction"
|
const val KEY_ACTION = "installerAction"
|
||||||
|
const val KEY_APP_NAME = "appName"
|
||||||
const val ACTION_UNINSTALL = "uninstall"
|
const val ACTION_UNINSTALL = "uninstall"
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
||||||
|
|
||||||
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
|
// only trigger a prompt if in foreground or below Android 10, otherwise make notification
|
||||||
// prompts user to enable unknown source
|
// launching a prompt in the background will fail silently
|
||||||
|
if ((!Android.sdk(29) || Utils.inForeground()) && status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
|
||||||
|
// Triggers the installer prompt and "unknown apps" prompt if needed
|
||||||
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
|
|
||||||
promptIntent?.let {
|
promptIntent?.let {
|
||||||
@ -45,7 +54,10 @@ class InstallerService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies user of installer outcome.
|
* Notifies user of installer outcome. This can be success, error, or a request for user action
|
||||||
|
* if installation cannot proceed automatically.
|
||||||
|
*
|
||||||
|
* @param intent provided by PackageInstaller to the callback service/activity.
|
||||||
*/
|
*/
|
||||||
private fun notifyStatus(intent: Intent) {
|
private fun notifyStatus(intent: Intent) {
|
||||||
// unpack from intent
|
// unpack from intent
|
||||||
@ -55,22 +67,23 @@ class InstallerService : Service() {
|
|||||||
val installerAction = intent.getStringExtra(KEY_ACTION)
|
val installerAction = intent.getStringExtra(KEY_ACTION)
|
||||||
|
|
||||||
// get application name for notifications
|
// get application name for notifications
|
||||||
val appLabel = try {
|
val appLabel = intent.getStringExtra(KEY_APP_NAME)
|
||||||
if (name != null) packageManager.getApplicationLabel(
|
?: try {
|
||||||
packageManager.getApplicationInfo(
|
if (name != null) packageManager.getApplicationLabel(
|
||||||
name,
|
packageManager.getApplicationInfo(
|
||||||
PackageManager.GET_META_DATA
|
name,
|
||||||
)
|
PackageManager.GET_META_DATA
|
||||||
) else null
|
)
|
||||||
} catch (_: Exception) {
|
) else null
|
||||||
null
|
} catch (_: Exception) {
|
||||||
}
|
null
|
||||||
|
}
|
||||||
|
|
||||||
val notificationTag = "download-$name"
|
val notificationTag = "download-$name"
|
||||||
|
|
||||||
// start building
|
// start building
|
||||||
val builder = NotificationCompat
|
val builder = NotificationCompat
|
||||||
.Builder(this, Common.NOTIFICATION_CHANNEL_DOWNLOADING)
|
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setColor(
|
.setColor(
|
||||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||||
@ -78,10 +91,21 @@ class InstallerService : Service() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
when (status) {
|
when (status) {
|
||||||
|
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||||
|
// request user action with "downloaded" notification that triggers a working prompt
|
||||||
|
notificationManager.notify(
|
||||||
|
notificationTag, NOTIFICATION_ID_DOWNLOADING, builder
|
||||||
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
|
.setContentIntent(installIntent(intent))
|
||||||
|
.setContentTitle(getString(R.string.downloaded_FORMAT, appLabel))
|
||||||
|
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
PackageInstaller.STATUS_SUCCESS -> {
|
PackageInstaller.STATUS_SUCCESS -> {
|
||||||
if (installerAction == ACTION_UNINSTALL)
|
if (installerAction == ACTION_UNINSTALL)
|
||||||
// remove any notification for this app
|
// remove any notification for this app
|
||||||
notificationManager.cancel(notificationTag, Common.NOTIFICATION_ID_DOWNLOADING)
|
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
|
||||||
else {
|
else {
|
||||||
val notification = builder
|
val notification = builder
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
@ -90,11 +114,11 @@ class InstallerService : Service() {
|
|||||||
.build()
|
.build()
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
notificationTag,
|
notificationTag,
|
||||||
Common.NOTIFICATION_ID_DOWNLOADING,
|
NOTIFICATION_ID_DOWNLOADING,
|
||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
Thread.sleep(5000)
|
Thread.sleep(5000)
|
||||||
notificationManager.cancel(notificationTag, Common.NOTIFICATION_ID_DOWNLOADING)
|
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||||
@ -109,7 +133,7 @@ class InstallerService : Service() {
|
|||||||
.build()
|
.build()
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
notificationTag,
|
notificationTag,
|
||||||
Common.NOTIFICATION_ID_DOWNLOADING,
|
NOTIFICATION_ID_DOWNLOADING,
|
||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -120,5 +144,33 @@ class InstallerService : Service() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an intent that provides the specified activity information necessary to trigger
|
||||||
|
* the package manager's prompt, thus completing a staged installation requiring user
|
||||||
|
* intervention.
|
||||||
|
*
|
||||||
|
* @param intent the intent provided by PackageInstaller to the callback target passed to
|
||||||
|
* PackageInstaller.Session.commit().
|
||||||
|
* @return a pending intent that can be attached to a background-accessible entry point such as
|
||||||
|
* a notification
|
||||||
|
*/
|
||||||
|
private fun installIntent(intent: Intent): PendingIntent {
|
||||||
|
// prepare prompt intent
|
||||||
|
val promptIntent : Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
|
val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
this,
|
||||||
|
0,
|
||||||
|
Intent(this, MainActivity::class.java)
|
||||||
|
.setAction(MainActivity.ACTION_INSTALL)
|
||||||
|
.setData(Uri.parse("package:$name"))
|
||||||
|
.putExtra(Intent.EXTRA_INTENT, promptIntent)
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
|
if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
else PendingIntent.FLAG_UPDATE_CURRENT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -219,11 +219,13 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
.getColorFromAttr(R.attr.colorPrimary).defaultColor
|
.getColorFromAttr(R.attr.colorPrimary).defaultColor
|
||||||
)
|
)
|
||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
PendingIntent.getBroadcast(
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
Intent(this, Receiver::class.java)
|
Intent(this, MainActivity::class.java)
|
||||||
.setAction("$ACTION_OPEN.${task.packageName}"),
|
.setAction(Intent.ACTION_VIEW)
|
||||||
|
.setData(Uri.parse("package:${task.packageName}"))
|
||||||
|
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||||
if (Android.sdk(23))
|
if (Android.sdk(23))
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
else
|
else
|
||||||
@ -308,12 +310,10 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
consumed = true
|
consumed = true
|
||||||
}
|
}
|
||||||
if (!consumed) {
|
if (!consumed) {
|
||||||
if (rootInstallerEnabled) {
|
scope.launch {
|
||||||
scope.launch {
|
AppInstaller.getInstance(this@DownloadService)
|
||||||
AppInstaller.getInstance(this@DownloadService)
|
?.defaultInstaller?.install(task.name, task.release.cacheFileName)
|
||||||
?.defaultInstaller?.install(task.release.cacheFileName)
|
}
|
||||||
}
|
|
||||||
} else showNotificationInstall(task)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package com.looker.droidify.utility
|
package com.looker.droidify.utility
|
||||||
|
|
||||||
|
import android.app.ActivityManager
|
||||||
|
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||||
|
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.Signature
|
import android.content.pm.Signature
|
||||||
@ -166,4 +169,15 @@ object Utils {
|
|||||||
)
|
)
|
||||||
else -> Locale(localeCode)
|
else -> Locale(localeCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if app is currently considered to be in the foreground by Android.
|
||||||
|
*/
|
||||||
|
fun inForeground(): Boolean {
|
||||||
|
val appProcessInfo = ActivityManager.RunningAppProcessInfo()
|
||||||
|
ActivityManager.getMyMemoryState(appProcessInfo)
|
||||||
|
val importance = appProcessInfo.importance
|
||||||
|
return ((importance == IMPORTANCE_FOREGROUND) or (importance == IMPORTANCE_VISIBLE))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user