mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-04-24 03:42:15 +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_SYNCING = "syncing"
|
||||||
const val NOTIFICATION_CHANNEL_UPDATES = "updates"
|
const val NOTIFICATION_CHANNEL_UPDATES = "updates"
|
||||||
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
|
const val NOTIFICATION_CHANNEL_DOWNLOADING = "downloading"
|
||||||
|
const val NOTIFICATION_CHANNEL_INSTALLER = "installed"
|
||||||
|
|
||||||
const val NOTIFICATION_ID_SYNCING = 1
|
const val NOTIFICATION_ID_SYNCING = 1
|
||||||
const val NOTIFICATION_ID_UPDATES = 2
|
const val NOTIFICATION_ID_UPDATES = 2
|
||||||
const val NOTIFICATION_ID_DOWNLOADING = 3
|
const val NOTIFICATION_ID_DOWNLOADING = 3
|
||||||
|
const val NOTIFICATION_ID_INSTALLER = 4
|
||||||
|
|
||||||
const val ROW_REPOSITORY_ID = "repository_id"
|
const val ROW_REPOSITORY_ID = "repository_id"
|
||||||
const val ROW_PACKAGE_NAME = "package_name"
|
const val ROW_PACKAGE_NAME = "package_name"
|
||||||
|
@ -2,6 +2,7 @@ package com.looker.droidify
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import com.looker.droidify.ContextWrapperX.Companion.wrap
|
import com.looker.droidify.ContextWrapperX.Companion.wrap
|
||||||
import com.looker.droidify.screen.ScreenActivity
|
import com.looker.droidify.screen.ScreenActivity
|
||||||
|
|
||||||
@ -19,7 +20,8 @@ class MainActivity : ScreenActivity() {
|
|||||||
ACTION_INSTALL -> handleSpecialIntent(
|
ACTION_INSTALL -> handleSpecialIntent(
|
||||||
SpecialIntent.Install(
|
SpecialIntent.Install(
|
||||||
intent.packageName,
|
intent.packageName,
|
||||||
intent.getStringExtra(EXTRA_CACHE_FILE_NAME)
|
intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1),
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else -> super.handleIntent(intent)
|
else -> super.handleIntent(intent)
|
||||||
|
@ -24,6 +24,7 @@ object Preferences {
|
|||||||
private val keys = sequenceOf(
|
private val keys = sequenceOf(
|
||||||
Key.Language,
|
Key.Language,
|
||||||
Key.AutoSync,
|
Key.AutoSync,
|
||||||
|
Key.InstallAfterSync,
|
||||||
Key.IncompatibleVersions,
|
Key.IncompatibleVersions,
|
||||||
Key.ListAnimation,
|
Key.ListAnimation,
|
||||||
Key.ProxyHost,
|
Key.ProxyHost,
|
||||||
@ -132,6 +133,8 @@ object Preferences {
|
|||||||
"auto_sync",
|
"auto_sync",
|
||||||
Value.EnumerationValue(Preferences.AutoSync.Wifi)
|
Value.EnumerationValue(Preferences.AutoSync.Wifi)
|
||||||
)
|
)
|
||||||
|
object InstallAfterSync :
|
||||||
|
Key<Boolean>("auto_sync_install", Value.BooleanValue(Android.sdk(31)))
|
||||||
|
|
||||||
object IncompatibleVersions :
|
object IncompatibleVersions :
|
||||||
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
||||||
|
@ -4,15 +4,19 @@ 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.SessionParams
|
import android.content.pm.PackageInstaller.SessionParams
|
||||||
|
import android.util.Log
|
||||||
import com.looker.droidify.content.Cache
|
import com.looker.droidify.content.Cache
|
||||||
import com.looker.droidify.utility.extension.android.Android
|
import com.looker.droidify.utility.extension.android.Android
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
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)
|
private val intent = Intent(context, InstallerService::class.java)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -33,32 +37,85 @@ 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)
|
||||||
|
|
||||||
private fun mDefaultInstaller(cacheFile: File) {
|
private fun mDefaultInstaller(cacheFile: File) {
|
||||||
|
// clean up inactive sessions
|
||||||
val id = sessionInstaller.createSession(sessionParams)
|
sessionInstaller.mySessions
|
||||||
|
.filter { session -> !session.isActive }
|
||||||
val session = sessionInstaller.openSession(id)
|
.forEach { session ->
|
||||||
|
try {
|
||||||
session.use { activeSession ->
|
sessionInstaller.abandonSession(session.sessionId)
|
||||||
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
|
}
|
||||||
cacheFile.inputStream().use { fileStream ->
|
catch (_: SecurityException) {
|
||||||
fileStream.copyTo(packageStream)
|
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) {
|
private suspend fun mDefaultUninstaller(packageName: String) {
|
||||||
|
@ -1,33 +1,56 @@
|
|||||||
package com.looker.droidify.installer
|
package com.looker.droidify.installer
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
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.NOTIFICATION_CHANNEL_DOWNLOADING
|
import com.looker.droidify.NOTIFICATION_CHANNEL_INSTALLER
|
||||||
import com.looker.droidify.NOTIFICATION_ID_DOWNLOADING
|
import com.looker.droidify.NOTIFICATION_ID_INSTALLER
|
||||||
|
import com.looker.droidify.MainActivity
|
||||||
import com.looker.droidify.R
|
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.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"
|
||||||
|
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 {
|
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, otherwise make notification
|
||||||
// prompts user to enable unknown source
|
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)
|
val promptIntent: Intent? = intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||||
|
|
||||||
promptIntent?.let {
|
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) {
|
private fun notifyStatus(intent: Intent) {
|
||||||
// unpack from intent
|
// unpack from intent
|
||||||
val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)
|
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 message = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
|
||||||
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 = session?.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 = "${NOTIFICATION_TAG_PREFIX}$name"
|
||||||
|
|
||||||
// start building
|
// start building
|
||||||
val builder = NotificationCompat
|
val builder = NotificationCompat
|
||||||
.Builder(this, NOTIFICATION_CHANNEL_DOWNLOADING)
|
.Builder(this, NOTIFICATION_CHANNEL_INSTALLER)
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setColor(
|
.setColor(
|
||||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||||
@ -79,23 +112,33 @@ 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_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 -> {
|
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, NOTIFICATION_ID_DOWNLOADING)
|
notificationManager.cancel(notificationTag, NOTIFICATION_ID_INSTALLER)
|
||||||
else {
|
else {
|
||||||
val notification = builder
|
val notification = builder
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
.setContentTitle(getString(R.string.installed))
|
.setContentTitle(getString(R.string.installed))
|
||||||
.setContentText(appLabel)
|
.setContentText(appLabel)
|
||||||
|
.setTimeoutAfter(INSTALLED_NOTIFICATION_TIMEOUT)
|
||||||
.build()
|
.build()
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
notificationTag,
|
notificationTag,
|
||||||
NOTIFICATION_ID_DOWNLOADING,
|
NOTIFICATION_ID_INSTALLER,
|
||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
Thread.sleep(5000)
|
|
||||||
notificationManager.cancel(notificationTag, NOTIFICATION_ID_DOWNLOADING)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
PackageInstaller.STATUS_FAILURE_ABORTED -> {
|
||||||
@ -110,7 +153,7 @@ class InstallerService : Service() {
|
|||||||
.build()
|
.build()
|
||||||
notificationManager.notify(
|
notificationManager.notify(
|
||||||
notificationTag,
|
notificationTag,
|
||||||
NOTIFICATION_ID_DOWNLOADING,
|
NOTIFICATION_ID_INSTALLER,
|
||||||
notification
|
notification
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -121,5 +164,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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package com.looker.droidify.screen
|
package com.looker.droidify.screen
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.content.pm.PackageInstaller
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcel
|
import android.os.Parcel
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
@ -14,7 +15,7 @@ import com.looker.droidify.MainApplication
|
|||||||
import com.looker.droidify.R
|
import com.looker.droidify.R
|
||||||
import com.looker.droidify.content.Preferences
|
import com.looker.droidify.content.Preferences
|
||||||
import com.looker.droidify.database.CursorOwner
|
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.ui.fragments.AppDetailFragment
|
||||||
import com.looker.droidify.utility.KParcelable
|
import com.looker.droidify.utility.KParcelable
|
||||||
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
|
||||||
@ -31,7 +32,7 @@ abstract class ScreenActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
sealed class SpecialIntent {
|
sealed class SpecialIntent {
|
||||||
object Updates : 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(
|
private class FragmentStackItem(
|
||||||
@ -221,14 +222,24 @@ abstract class ScreenActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
is SpecialIntent.Install -> {
|
is SpecialIntent.Install -> {
|
||||||
val packageName = specialIntent.packageName
|
val packageName = specialIntent.packageName
|
||||||
if (!packageName.isNullOrEmpty()) {
|
val status = specialIntent.status
|
||||||
|
val promptIntent = specialIntent.promptIntent
|
||||||
|
if (!packageName.isNullOrEmpty() && status != null && promptIntent != null) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
specialIntent.cacheFileName?.let {
|
startService(
|
||||||
AppInstaller.getInstance(this@ScreenActivity)
|
Intent(baseContext, InstallerService::class.java)
|
||||||
?.defaultInstaller?.install(packageName, it)
|
.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
|
Unit
|
||||||
}
|
}
|
||||||
}::class
|
}::class
|
||||||
|
@ -108,6 +108,10 @@ class SettingsFragment : ScreenFragment() {
|
|||||||
Preferences.AutoSync.Always -> getString(R.string.always)
|
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(
|
addSwitch(
|
||||||
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
|
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
|
||||||
getString(R.string.notify_about_updates_summary)
|
getString(R.string.notify_about_updates_summary)
|
||||||
|
@ -3,8 +3,6 @@ package com.looker.droidify.service
|
|||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.view.ContextThemeWrapper
|
import android.view.ContextThemeWrapper
|
||||||
@ -16,7 +14,6 @@ import com.looker.droidify.entity.Repository
|
|||||||
import com.looker.droidify.installer.AppInstaller
|
import com.looker.droidify.installer.AppInstaller
|
||||||
import com.looker.droidify.network.Downloader
|
import com.looker.droidify.network.Downloader
|
||||||
import com.looker.droidify.utility.Utils
|
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.android.*
|
||||||
import com.looker.droidify.utility.extension.resources.*
|
import com.looker.droidify.utility.extension.resources.*
|
||||||
import com.looker.droidify.utility.extension.text.*
|
import com.looker.droidify.utility.extension.text.*
|
||||||
@ -30,11 +27,7 @@ import kotlin.math.*
|
|||||||
|
|
||||||
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
class DownloadService : ConnectionService<DownloadService.Binder>() {
|
||||||
companion object {
|
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 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 mutableDownloadState = MutableSharedFlow<State.Downloading>()
|
||||||
private val downloadState = mutableDownloadState.asSharedFlow()
|
private val downloadState = mutableDownloadState.asSharedFlow()
|
||||||
@ -43,34 +36,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
private val scope = CoroutineScope(Dispatchers.Default)
|
private val scope = CoroutineScope(Dispatchers.Default)
|
||||||
private val mainDispatcher = Dispatchers.Main
|
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) {
|
sealed class State(val packageName: String, val name: String) {
|
||||||
class Pending(packageName: String, name: String) : State(packageName, name)
|
class Pending(packageName: String, name: String) : State(packageName, name)
|
||||||
class Connecting(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
|
.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
|
||||||
@ -275,33 +242,6 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
|
|||||||
.build())
|
.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) {
|
private fun publishSuccess(task: Task) {
|
||||||
var consumed = false
|
var consumed = false
|
||||||
scope.launch(mainDispatcher) {
|
scope.launch(mainDispatcher) {
|
||||||
@ -309,12 +249,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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import com.looker.droidify.entity.ProductItem
|
|||||||
import com.looker.droidify.entity.Repository
|
import com.looker.droidify.entity.Repository
|
||||||
import com.looker.droidify.index.RepositoryUpdater
|
import com.looker.droidify.index.RepositoryUpdater
|
||||||
import com.looker.droidify.utility.RxUtils
|
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.Android
|
||||||
import com.looker.droidify.utility.extension.android.asSequence
|
import com.looker.droidify.utility.extension.android.asSequence
|
||||||
import com.looker.droidify.utility.extension.android.notificationManager
|
import com.looker.droidify.utility.extension.android.notificationManager
|
||||||
@ -70,6 +71,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
|
|
||||||
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
|
private var updateNotificationBlockerFragment: WeakReference<Fragment>? = null
|
||||||
|
|
||||||
|
private val downloadConnection = Connection(DownloadService::class.java)
|
||||||
|
|
||||||
enum class SyncRequest { AUTO, MANUAL, FORCE }
|
enum class SyncRequest { AUTO, MANUAL, FORCE }
|
||||||
|
|
||||||
inner class Binder : android.os.Binder() {
|
inner class Binder : android.os.Binder() {
|
||||||
@ -172,11 +175,13 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
.let(notificationManager::createNotificationChannel)
|
.let(notificationManager::createNotificationChannel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
downloadConnection.bind(this)
|
||||||
stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope)
|
stateSubject.onEach { publishForegroundState(false, it) }.launchIn(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
|
downloadConnection.unbind(this)
|
||||||
cancelTasks { true }
|
cancelTasks { true }
|
||||||
cancelCurrentTask { true }
|
cancelCurrentTask { true }
|
||||||
}
|
}
|
||||||
@ -372,33 +377,38 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
|||||||
handleNextTask(hasUpdates)
|
handleNextTask(hasUpdates)
|
||||||
}
|
}
|
||||||
} else if (started != Started.NO) {
|
} else if (started != Started.NO) {
|
||||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
val disposable = RxUtils
|
||||||
val disposable = RxUtils
|
.querySingle { it ->
|
||||||
.querySingle { it ->
|
db.productDao
|
||||||
db.productDao
|
.query(
|
||||||
.query(
|
installed = true,
|
||||||
installed = true,
|
updates = true,
|
||||||
updates = true,
|
searchQuery = "",
|
||||||
searchQuery = "",
|
section = ProductItem.Section.All,
|
||||||
section = ProductItem.Section.All,
|
order = ProductItem.Order.NAME,
|
||||||
order = ProductItem.Order.NAME,
|
signal = it
|
||||||
signal = it
|
)
|
||||||
)
|
.use {
|
||||||
.use {
|
it.asSequence().map { it.getProductItem() }
|
||||||
it.asSequence().map { it.getProductItem() }.toList()
|
.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)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.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)
|
currentTask = CurrentTask(null, disposable, true, State.Finishing)
|
||||||
} else {
|
} else {
|
||||||
scope.launch { mutableFinishState.emit(Unit) }
|
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>) {
|
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||||
val maxUpdates = 5
|
val maxUpdates = 5
|
||||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||||
|
@ -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
|
||||||
@ -172,6 +175,17 @@ 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))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))
|
fun Cursor.getProduct(): Product = getBlob(getColumnIndex(ROW_DATA))
|
||||||
|
@ -181,4 +181,6 @@
|
|||||||
<string name="installed_applications">Installed applications</string>
|
<string name="installed_applications">Installed applications</string>
|
||||||
<string name="sort_filter">Sort & Filter</string>
|
<string name="sort_filter">Sort & Filter</string>
|
||||||
<string name="new_applications">New applications</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>
|
</resources>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user