mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-04-23 19:32:16 +00:00
This commit is contained in:
commit
8dc4dcce0f
@ -3,10 +3,12 @@ package com.looker.droidify
|
||||
const val NOTIFICATION_CHANNEL_SYNCING = "syncing"
|
||||
const val NOTIFICATION_CHANNEL_UPDATES = "updates"
|
||||
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
|
||||
const val NOTIFICATION_CHANNEL_INSTALLER = "installed"
|
||||
|
||||
const val NOTIFICATION_ID_SYNCING = 1
|
||||
const val NOTIFICATION_ID_UPDATES = 2
|
||||
const val NOTIFICATION_ID_DOWNLOADING = 3
|
||||
const val NOTIFICATION_ID_INSTALLER = 4
|
||||
|
||||
const val ROW_REPOSITORY_ID = "repository_id"
|
||||
const val ROW_PACKAGE_NAME = "package_name"
|
||||
|
@ -2,6 +2,7 @@ package com.looker.droidify
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import com.looker.droidify.ContextWrapperX.Companion.wrap
|
||||
import com.looker.droidify.screen.ScreenActivity
|
||||
|
||||
@ -19,7 +20,8 @@ class MainActivity : ScreenActivity() {
|
||||
ACTION_INSTALL -> handleSpecialIntent(
|
||||
SpecialIntent.Install(
|
||||
intent.packageName,
|
||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1),
|
||||
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
)
|
||||
)
|
||||
else -> super.handleIntent(intent)
|
||||
|
@ -24,6 +24,7 @@ object Preferences {
|
||||
private val keys = sequenceOf(
|
||||
Key.Language,
|
||||
Key.AutoSync,
|
||||
Key.InstallAfterSync,
|
||||
Key.IncompatibleVersions,
|
||||
Key.ListAnimation,
|
||||
Key.ProxyHost,
|
||||
@ -132,6 +133,8 @@ object Preferences {
|
||||
"auto_sync",
|
||||
Value.EnumerationValue(Preferences.AutoSync.Wifi)
|
||||
)
|
||||
object InstallAfterSync :
|
||||
Key<Boolean>("auto_sync_install", Value.BooleanValue(Android.sdk(31)))
|
||||
|
||||
object IncompatibleVersions :
|
||||
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
||||
|
@ -4,15 +4,19 @@ import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller.SessionParams
|
||||
import android.util.Log
|
||||
import com.looker.droidify.content.Cache
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
||||
class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
||||
|
||||
private val sessionInstaller = context.packageManager.packageInstaller
|
||||
private val packageManager = context.packageManager
|
||||
private val sessionInstaller = packageManager.packageInstaller
|
||||
private val intent = Intent(context, InstallerService::class.java)
|
||||
|
||||
companion object {
|
||||
@ -33,32 +37,85 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
||||
|
||||
override suspend fun install(packageName: String, cacheFileName: String) {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
override suspend fun uninstall(packageName: String) = mDefaultUninstaller(packageName)
|
||||
|
||||
private fun mDefaultInstaller(cacheFile: File) {
|
||||
|
||||
val id = sessionInstaller.createSession(sessionParams)
|
||||
|
||||
val session = sessionInstaller.openSession(id)
|
||||
|
||||
session.use { activeSession ->
|
||||
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
|
||||
cacheFile.inputStream().use { fileStream ->
|
||||
fileStream.copyTo(packageStream)
|
||||
// clean up inactive sessions
|
||||
sessionInstaller.mySessions
|
||||
.filter { session -> !session.isActive }
|
||||
.forEach { session ->
|
||||
try {
|
||||
sessionInstaller.abandonSession(session.sessionId)
|
||||
}
|
||||
catch (_: SecurityException) {
|
||||
Log.w(
|
||||
"DefaultInstaller",
|
||||
"Attempted to abandon a session we do not own."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
|
||||
// start new session
|
||||
val id = sessionInstaller.createSession(sessionParams)
|
||||
val session = sessionInstaller.openSession(id)
|
||||
|
||||
session.commit(pendingIntent.intentSender)
|
||||
// get package name
|
||||
val packageInfo = packageManager.getPackageArchiveInfo(cacheFile.absolutePath, 0)
|
||||
val packageName = packageInfo?.packageName ?: "unknown-package"
|
||||
|
||||
// error flags
|
||||
var hasErrors = false
|
||||
|
||||
session.use { activeSession ->
|
||||
try {
|
||||
activeSession.openWrite(packageName, 0, cacheFile.length()).use { packageStream ->
|
||||
try {
|
||||
cacheFile.inputStream().use { fileStream ->
|
||||
fileStream.copyTo(packageStream)
|
||||
}
|
||||
} catch (_: FileNotFoundException) {
|
||||
Log.w(
|
||||
"DefaultInstaller",
|
||||
"Cache file does not seem to exist."
|
||||
)
|
||||
hasErrors = true
|
||||
} catch (_: IOException) {
|
||||
Log.w(
|
||||
"DefaultInstaller",
|
||||
"Failed to perform cache to package copy due to a bad pipe."
|
||||
)
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
} catch (_: SecurityException) {
|
||||
Log.w(
|
||||
"DefaultInstaller",
|
||||
"Attempted to use a destroyed or sealed session when installing."
|
||||
)
|
||||
hasErrors = true
|
||||
} catch (_: IOException) {
|
||||
Log.w(
|
||||
"DefaultInstaller",
|
||||
"Couldn't open up active session file for copying install data."
|
||||
)
|
||||
hasErrors = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
session.commit(PendingIntent.getService(context, id, intent, flags).intentSender)
|
||||
cacheFile.delete()
|
||||
}
|
||||
cacheFile.delete()
|
||||
}
|
||||
|
||||
private suspend fun mDefaultUninstaller(packageName: String) {
|
||||
|
@ -1,33 +1,56 @@
|
||||
package com.looker.droidify.installer
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.IBinder
|
||||
import android.view.ContextThemeWrapper
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.looker.droidify.NOTIFICATION_CHANNEL_DOWNLOADING
|
||||
import com.looker.droidify.NOTIFICATION_ID_DOWNLOADING
|
||||
import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER
|
||||
import com.looker.droidify.NOTIFICATION_ID_INSTALLER
|
||||
import com.looker.droidify.MainActivity
|
||||
import com.looker.droidify.R
|
||||
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.resources.getColorFromAttr
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
companion object {
|
||||
const val KEY_ACTION = "installerAction"
|
||||
const val KEY_APP_NAME = "appName"
|
||||
const val ACTION_UNINSTALL = "uninstall"
|
||||
private const val INSTALLED_NOTIFICATION_TIMEOUT: Long = 5000
|
||||
private const val NOTIFICATION_TAG_PREFIX = "install-"
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (Android.sdk(26)) {
|
||||
NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_INSTALLER,
|
||||
getString(R.string.syncing), NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
||||
|
||||
if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) {
|
||||
// prompts user to enable unknown source
|
||||
// only trigger a prompt if in foreground, otherwise make notification
|
||||
if (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)
|
||||
|
||||
promptIntent?.let {
|
||||
@ -46,32 +69,42 @@ 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) {
|
||||
// unpack from intent
|
||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
||||
val name = intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
val sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1)
|
||||
|
||||
// get package information from session
|
||||
val sessionInstaller = this.packageManager.packageInstaller
|
||||
val session = if (sessionId > 0) sessionInstaller.getSessionInfo(sessionId) else null
|
||||
|
||||
val name = session?.appPackageName ?: intent.getStringExtra(PackageInstaller.EXTRA_PACKAGE_NAME)
|
||||
val message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||
val installerAction = intent.getStringExtra(KEY_ACTION)
|
||||
|
||||
// get application name for notifications
|
||||
val appLabel = try {
|
||||
if (name != null) packageManager.getApplicationLabel(
|
||||
packageManager.getApplicationInfo(
|
||||
name,
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
) else null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
val appLabel = session?.appLabel ?: intent.getStringExtra(KEY_APP_NAME)
|
||||
?: try {
|
||||
if (name != null) packageManager.getApplicationLabel(
|
||||
packageManager.getApplicationInfo(
|
||||
name,
|
||||
PackageManager.GET_META_DATA
|
||||
)
|
||||
) else null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val notificationTag = "download-$name"
|
||||
val notificationTag = "${NOTIFICATION_TAG_PREFIX}$name"
|
||||
|
||||
// start building
|
||||
val builder = NotificationCompat
|
||||
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.Builder(this, NOTIFICATION_CHANNEL_INSTALLER)
|
||||
.setAutoCancel(true)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
@ -79,23 +112,33 @@ class InstallerService : Service() {
|
||||
)
|
||||
|
||||
when (status) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
// request user action with "downloaded" notification that triggers a working prompt
|
||||
notificationManager.notify(
|
||||
notificationTag, NOTIFICATION_ID_INSTALLER, 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 -> {
|
||||
if (installerAction == ACTION_UNINSTALL)
|
||||
// remove any notification for this app
|
||||
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
|
||||
notificationManager.cancel(notificationTag, NOTIFICATION_ID_INSTALLER)
|
||||
else {
|
||||
val notification = builder
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setContentTitle(getString(R.string.installed))
|
||||
.setContentText(appLabel)
|
||||
.setTimeoutAfter(INSTALLED_NOTIFICATION_TIMEOUT)
|
||||
.build()
|
||||
notificationManager.notify(
|
||||
notificationTag,
|
||||
NOTIFICATION_ID_DOWNLOADING,
|
||||
NOTIFICATION_ID_INSTALLER,
|
||||
notification
|
||||
)
|
||||
Thread.sleep(5000)
|
||||
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
|
||||
}
|
||||
}
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||
@ -110,7 +153,7 @@ class InstallerService : Service() {
|
||||
.build()
|
||||
notificationManager.notify(
|
||||
notificationTag,
|
||||
NOTIFICATION_ID_DOWNLOADING,
|
||||
NOTIFICATION_ID_INSTALLER,
|
||||
notification
|
||||
)
|
||||
}
|
||||
@ -121,5 +164,33 @@ class InstallerService : Service() {
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.looker.droidify.screen
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import android.view.ViewGroup
|
||||
@ -14,7 +15,7 @@ import com.looker.droidify.MainApplication
|
||||
import com.looker.droidify.R
|
||||
import com.looker.droidify.content.Preferences
|
||||
import com.looker.droidify.database.CursorOwner
|
||||
import com.looker.droidify.installer.AppInstaller
|
||||
import com.looker.droidify.installer.InstallerService
|
||||
import com.looker.droidify.ui.fragments.AppDetailFragment
|
||||
import com.looker.droidify.utility.KParcelable
|
||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
||||
@ -31,7 +32,7 @@ abstract class ScreenActivity : AppCompatActivity() {
|
||||
|
||||
sealed class SpecialIntent {
|
||||
object Updates : SpecialIntent()
|
||||
class Install(val packageName: String?, val cacheFileName: String?) : SpecialIntent()
|
||||
class Install(val packageName: String?, val status: Int?, val promptIntent: Intent?) : SpecialIntent()
|
||||
}
|
||||
|
||||
private class FragmentStackItem(
|
||||
@ -221,14 +222,24 @@ abstract class ScreenActivity : AppCompatActivity() {
|
||||
}
|
||||
is SpecialIntent.Install -> {
|
||||
val packageName = specialIntent.packageName
|
||||
if (!packageName.isNullOrEmpty()) {
|
||||
val status = specialIntent.status
|
||||
val promptIntent = specialIntent.promptIntent
|
||||
if (!packageName.isNullOrEmpty() && status != null && promptIntent != null) {
|
||||
lifecycleScope.launch {
|
||||
specialIntent.cacheFileName?.let {
|
||||
AppInstaller.getInstance(this@ScreenActivity)
|
||||
?.defaultInstaller?.install(packageName, it)
|
||||
}
|
||||
startService(
|
||||
Intent(baseContext, InstallerService::class.java)
|
||||
.putExtra(PackageInstaller.EXTRA_STATUS, status)
|
||||
.putExtra(
|
||||
PackageInstaller.EXTRA_PACKAGE_NAME,
|
||||
packageName
|
||||
)
|
||||
.putExtra(Intent.EXTRA_INTENT, promptIntent)
|
||||
)
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw IllegalArgumentException("Missing parameters needed to relaunch InstallerService and trigger prompt.")
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}::class
|
||||
|
@ -108,6 +108,10 @@ class SettingsFragment : ScreenFragment() {
|
||||
Preferences.AutoSync.Always -> getString(R.string.always)
|
||||
}
|
||||
}
|
||||
addSwitch(
|
||||
Preferences.Key.InstallAfterSync, getString(R.string.install_after_sync),
|
||||
getString(R.string.install_after_sync_summary)
|
||||
)
|
||||
addSwitch(
|
||||
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
|
||||
getString(R.string.notify_about_updates_summary)
|
||||
|
@ -3,8 +3,6 @@ package com.looker.droidify.service
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.ContextThemeWrapper
|
||||
@ -16,7 +14,6 @@ import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.installer.AppInstaller
|
||||
import com.looker.droidify.network.Downloader
|
||||
import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.Utils.rootInstallerEnabled
|
||||
import com.looker.droidify.utility.extension.android.*
|
||||
import com.looker.droidify.utility.extension.resources.*
|
||||
import com.looker.droidify.utility.extension.text.*
|
||||
@ -30,11 +27,7 @@ import kotlin.math.*
|
||||
|
||||
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
companion object {
|
||||
private const val ACTION_OPEN = "${BuildConfig.APPLICATION_ID}.intent.action.OPEN"
|
||||
private const val ACTION_INSTALL = "${BuildConfig.APPLICATION_ID}.intent.action.INSTALL"
|
||||
private const val ACTION_CANCEL = "${BuildConfig.APPLICATION_ID}.intent.action.CANCEL"
|
||||
private const val EXTRA_CACHE_FILE_NAME =
|
||||
"${BuildConfig.APPLICATION_ID}.intent.extra.CACHE_FILE_NAME"
|
||||
|
||||
private val mutableDownloadState = MutableSharedFlow<State.Downloading>()
|
||||
private val downloadState = mutableDownloadState.asSharedFlow()
|
||||
@ -43,34 +36,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
private val scope = CoroutineScope(Dispatchers.Default)
|
||||
private val mainDispatcher = Dispatchers.Main
|
||||
|
||||
class Receiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action.orEmpty()
|
||||
when {
|
||||
action.startsWith("$ACTION_OPEN.") -> {
|
||||
val packageName = action.substring(ACTION_OPEN.length + 1)
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
action.startsWith("$ACTION_INSTALL.") -> {
|
||||
val packageName = action.substring(ACTION_INSTALL.length + 1)
|
||||
val cacheFileName = intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
||||
context.startActivity(
|
||||
Intent(context, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_INSTALL)
|
||||
.setData(Uri.parse("package:$packageName"))
|
||||
.putExtra(MainActivity.EXTRA_CACHE_FILE_NAME, cacheFileName)
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class State(val packageName: String, val name: String) {
|
||||
class Pending(packageName: String, name: String) : State(packageName, name)
|
||||
class Connecting(packageName: String, name: String) : State(packageName, name)
|
||||
@ -220,11 +185,13 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
.getColorFromAttr(R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getBroadcast(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_OPEN.${task.packageName}"),
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(Intent.ACTION_VIEW)
|
||||
.setData(Uri.parse("package:${task.packageName}"))
|
||||
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
|
||||
if (Android.sdk(23))
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
else
|
||||
@ -275,33 +242,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
.build())
|
||||
}
|
||||
|
||||
private fun showNotificationInstall(task: Task) {
|
||||
notificationManager.notify(
|
||||
task.notificationTag, NOTIFICATION_ID_DOWNLOADING, NotificationCompat
|
||||
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
|
||||
.setAutoCancel(true)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentIntent(installIntent(task))
|
||||
.setContentTitle(getString(R.string.downloaded_FORMAT, task.name))
|
||||
.setContentText(getString(R.string.tap_to_install_DESC))
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
private fun installIntent(task: Task): PendingIntent = PendingIntent.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
Intent(this, Receiver::class.java)
|
||||
.setAction("$ACTION_INSTALL.${task.packageName}")
|
||||
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName),
|
||||
if (Android.sdk(23)) PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
else PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
|
||||
private fun publishSuccess(task: Task) {
|
||||
var consumed = false
|
||||
scope.launch(mainDispatcher) {
|
||||
@ -309,12 +249,10 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||
consumed = true
|
||||
}
|
||||
if (!consumed) {
|
||||
if (rootInstallerEnabled) {
|
||||
scope.launch {
|
||||
AppInstaller.getInstance(this@DownloadService)
|
||||
?.defaultInstaller?.install(task.release.cacheFileName)
|
||||
}
|
||||
} else showNotificationInstall(task)
|
||||
scope.launch {
|
||||
AppInstaller.getInstance(this@DownloadService)
|
||||
?.defaultInstaller?.install(task.name, task.release.cacheFileName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,7 @@ import com.looker.droidify.entity.ProductItem
|
||||
import com.looker.droidify.entity.Repository
|
||||
import com.looker.droidify.index.RepositoryUpdater
|
||||
import com.looker.droidify.utility.RxUtils
|
||||
import com.looker.droidify.utility.Utils
|
||||
import com.looker.droidify.utility.extension.android.Android
|
||||
import com.looker.droidify.utility.extension.android.asSequence
|
||||
import com.looker.droidify.utility.extension.android.notificationManager
|
||||
@ -70,6 +71,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
|
||||
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
|
||||
|
||||
private val downloadConnection = Connection(DownloadService::class.java)
|
||||
|
||||
enum class SyncRequest { AUTO, MANUAL, FORCE }
|
||||
|
||||
inner class Binder : android.os.Binder() {
|
||||
@ -172,11 +175,13 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
.let(notificationManager::createNotificationChannel)
|
||||
}
|
||||
|
||||
downloadConnection.bind(this)
|
||||
stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
downloadConnection.unbind(this)
|
||||
cancelTasks { true }
|
||||
cancelCurrentTask { true }
|
||||
}
|
||||
@ -372,33 +377,38 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
handleNextTask(hasUpdates)
|
||||
}
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { it ->
|
||||
db.productDao
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = "",
|
||||
section = ProductItem.Section.All,
|
||||
order = ProductItem.Order.NAME,
|
||||
signal = it
|
||||
)
|
||||
.use {
|
||||
it.asSequence().map { it.getProductItem() }.toList()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
throwable?.printStackTrace()
|
||||
currentTask = null
|
||||
handleNextTask(false)
|
||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
if (!blocked && result != null && result.isNotEmpty()) {
|
||||
displayUpdatesNotification(result)
|
||||
val disposable = RxUtils
|
||||
.querySingle { it ->
|
||||
db.productDao
|
||||
.query(
|
||||
installed = true,
|
||||
updates = true,
|
||||
searchQuery = "",
|
||||
section = ProductItem.Section.All,
|
||||
order = ProductItem.Order.NAME,
|
||||
signal = it
|
||||
)
|
||||
.use {
|
||||
it.asSequence().map { it.getProductItem() }
|
||||
.toList()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { result, throwable ->
|
||||
throwable?.printStackTrace()
|
||||
currentTask = null
|
||||
handleNextTask(false)
|
||||
if (result.isNotEmpty()) {
|
||||
if (Preferences[Preferences.Key.InstallAfterSync])
|
||||
runAutoUpdate(result)
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify] &&
|
||||
updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
)
|
||||
displayUpdatesNotification(result)
|
||||
}
|
||||
}
|
||||
if (hasUpdates) {
|
||||
currentTask = CurrentTask(null, disposable, true, State.Finishing)
|
||||
} else {
|
||||
scope.launch { mutableFinishState.emit(Unit) }
|
||||
@ -413,6 +423,54 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs automatic update after a repo sync if it is enabled. Otherwise, it continues on to
|
||||
* displayUpdatesNotification.
|
||||
*
|
||||
* @param productItems a list of apps pending updates
|
||||
* @see SyncService.displayUpdatesNotification
|
||||
*/
|
||||
private fun runAutoUpdate(productItems: List<ProductItem>) {
|
||||
if (Preferences[Preferences.Key.InstallAfterSync]) {
|
||||
// run startUpdate on every item
|
||||
productItems.map { productItem ->
|
||||
Pair(
|
||||
Database.InstalledAdapter.get(productItem.packageName, null),
|
||||
Database.RepositoryAdapter.get(productItem.repositoryId)
|
||||
)
|
||||
}
|
||||
.filter { pair -> pair.first != null && pair.second != null }
|
||||
.forEach { installedRepository ->
|
||||
run {
|
||||
// Redundant !! as linter doesn't recognise the above filter's effects
|
||||
val installedItem = installedRepository.first!!
|
||||
val repository = installedRepository.second!!
|
||||
|
||||
val productRepository = Database.ProductAdapter.get(
|
||||
installedItem.packageName,
|
||||
null
|
||||
)
|
||||
.filter { product -> product.repositoryId == repository.id }
|
||||
.map { product -> Pair(product, repository) }
|
||||
|
||||
scope.launch {
|
||||
Utils.startUpdate(
|
||||
installedItem.packageName,
|
||||
installedRepository.first,
|
||||
productRepository,
|
||||
downloadConnection
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays summary of available updates.
|
||||
*
|
||||
* @param productItems list of apps pending updates
|
||||
*/
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
|
@ -1,5 +1,8 @@
|
||||
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.pm.PackageInfo
|
||||
import android.content.pm.Signature
|
||||
@ -172,6 +175,17 @@ object Utils {
|
||||
)
|
||||
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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))
|
||||
|
@ -181,4 +181,6 @@
|
||||
<string name="installed_applications">Installed applications</string>
|
||||
<string name="sort_filter">Sort & Filter</string>
|
||||
<string name="new_applications">New applications</string>
|
||||
<string name="install_after_sync">Install updates automatically</string>
|
||||
<string name="install_after_sync_summary">Automatically install app updates after syncing repositories</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user