mirror of
https://github.com/Aviortheking/Neo-Store.git
synced 2025-04-23 19:32:16 +00:00
Implement automatic updates after repository sync
Initial implementation of fully automatic updates, allowing apps to be updated after a repo sync has been completed. It can be enabled with a new preference in settings. Implemented by greatly modifying SyncService to allow updates to be run on every completed sync before notifications would be outputted. Note that the update notification no longer appears if auto updates are enabled. BuildConfig.DEBUG is used to force syncing to run to completion, even if there are no changes to the repos. This allow reliable testing by turning the sync button into an "update all" button. This should be removed once an "Update all" button has been implemented in the updates tab. Cache file checking in DefaultInstaller is now more robust.
This commit is contained in:
parent
adf305ffc0
commit
fce311098d
@ -24,6 +24,7 @@ object Preferences {
|
||||
private val keys = sequenceOf(
|
||||
Key.Language,
|
||||
Key.AutoSync,
|
||||
Key.AutoSyncInstall,
|
||||
Key.IncompatibleVersions,
|
||||
Key.ListAnimation,
|
||||
Key.ProxyHost,
|
||||
@ -130,6 +131,8 @@ object Preferences {
|
||||
"auto_sync",
|
||||
Value.EnumerationValue(Preferences.AutoSync.Wifi)
|
||||
)
|
||||
object AutoSyncInstall :
|
||||
Key<Boolean>("auto_sync_install", Value.BooleanValue(true))
|
||||
|
||||
object IncompatibleVersions :
|
||||
Key<Boolean>("incompatible_versions", Value.BooleanValue(false))
|
||||
|
@ -4,11 +4,13 @@ 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
|
||||
|
||||
class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
||||
|
||||
@ -51,18 +53,20 @@ class DefaultInstaller(context: Context) : BaseInstaller(context) {
|
||||
|
||||
val session = sessionInstaller.openSession(id)
|
||||
|
||||
if (cacheFile.exists()) {
|
||||
session.use { activeSession ->
|
||||
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
|
||||
session.use { activeSession ->
|
||||
activeSession.openWrite("package", 0, cacheFile.length()).use { packageStream ->
|
||||
try {
|
||||
cacheFile.inputStream().use { fileStream ->
|
||||
fileStream.copyTo(packageStream)
|
||||
}
|
||||
} catch (error: FileNotFoundException) {
|
||||
Log.w("DefaultInstaller", "Cache file for DefaultInstaller does not seem to exist.")
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
|
||||
|
||||
session.commit(pendingIntent.intentSender)
|
||||
}
|
||||
|
||||
val pendingIntent = PendingIntent.getService(context, id, intent, flags)
|
||||
|
||||
session.commit(pendingIntent.intentSender)
|
||||
}
|
||||
cacheFile.delete()
|
||||
}
|
||||
|
@ -103,6 +103,10 @@ class SettingsFragment : ScreenFragment() {
|
||||
Preferences.AutoSync.Always -> getString(R.string.always)
|
||||
}
|
||||
}
|
||||
addSwitch(
|
||||
Preferences.Key.AutoSyncInstall, getString(R.string.sync_auto_install),
|
||||
getString(R.string.sync_auto_install_summary)
|
||||
)
|
||||
addSwitch(
|
||||
Preferences.Key.UpdateNotify, getString(R.string.notify_about_updates),
|
||||
getString(R.string.notify_about_updates_summary)
|
||||
|
@ -23,6 +23,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
|
||||
@ -73,6 +74,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() {
|
||||
@ -173,11 +176,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 }
|
||||
}
|
||||
@ -366,14 +371,14 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
if (throwable != null && task.manual) {
|
||||
showNotificationError(repository, throwable as Exception)
|
||||
}
|
||||
handleNextTask(result == true || hasUpdates)
|
||||
handleNextTask(BuildConfig.DEBUG || result == true || hasUpdates)
|
||||
}
|
||||
currentTask = CurrentTask(task, disposable, hasUpdates, initialState)
|
||||
} else {
|
||||
handleNextTask(hasUpdates)
|
||||
}
|
||||
} else if (started != Started.NO) {
|
||||
if (hasUpdates && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
if (hasUpdates) {
|
||||
val disposable = RxUtils
|
||||
.querySingle { it ->
|
||||
Database.ProductAdapter
|
||||
@ -396,9 +401,8 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
throwable?.printStackTrace()
|
||||
currentTask = null
|
||||
handleNextTask(false)
|
||||
val blocked = updateNotificationBlockerFragment?.get()?.isAdded == true
|
||||
if (!blocked && result != null && result.isNotEmpty()) {
|
||||
displayUpdatesNotification(result)
|
||||
if (result.isNotEmpty()) {
|
||||
runAutoUpdate(result)
|
||||
}
|
||||
}
|
||||
currentTask = CurrentTask(null, disposable, true, State.Finishing)
|
||||
@ -415,58 +419,110 @@ class SyncService : ConnectionService<SyncService.Binder>() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager.notify(
|
||||
Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(R.string.new_updates_available))
|
||||
.setContentText(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size
|
||||
)
|
||||
/**
|
||||
* 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.AutoSyncInstall]) {
|
||||
// run startUpdate on every item
|
||||
productItems.map { productItem ->
|
||||
Pair(
|
||||
Database.InstalledAdapter.get(productItem.packageName, null),
|
||||
Database.RepositoryAdapter.get(productItem.repositoryId)
|
||||
)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
else
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(
|
||||
ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
}
|
||||
.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
|
||||
)
|
||||
builder.append(' ').append(productItem.version)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > maxUpdates) {
|
||||
val summary =
|
||||
getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
if (Android.sdk(24)) {
|
||||
addLine(summary)
|
||||
} else {
|
||||
setSummaryText(summary)
|
||||
.filter { product -> product.repositoryId == repository.id }
|
||||
.map { product -> Pair(product, repository) }
|
||||
|
||||
scope.launch {
|
||||
Utils.startUpdate(
|
||||
installedItem.packageName,
|
||||
installedRepository.first,
|
||||
productRepository,
|
||||
downloadConnection
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}
|
||||
} else {
|
||||
displayUpdatesNotification(productItems)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays summary of available updates.
|
||||
*
|
||||
* @param productItems list of apps pending updates
|
||||
*/
|
||||
private fun displayUpdatesNotification(productItems: List<ProductItem>) {
|
||||
if (updateNotificationBlockerFragment?.get()?.isAdded == true && Preferences[Preferences.Key.UpdateNotify]) {
|
||||
val maxUpdates = 5
|
||||
fun <T> T.applyHack(callback: T.() -> Unit): T = apply(callback)
|
||||
notificationManager.notify(
|
||||
Common.NOTIFICATION_ID_UPDATES, NotificationCompat
|
||||
.Builder(this, Common.NOTIFICATION_CHANNEL_UPDATES)
|
||||
.setSmallIcon(R.drawable.ic_new_releases)
|
||||
.setContentTitle(getString(R.string.new_updates_available))
|
||||
.setContentText(
|
||||
resources.getQuantityString(
|
||||
R.plurals.new_updates_DESC_FORMAT,
|
||||
productItems.size, productItems.size
|
||||
)
|
||||
)
|
||||
.setColor(
|
||||
ContextThemeWrapper(this, R.style.Theme_Main_Light)
|
||||
.getColorFromAttr(android.R.attr.colorPrimary).defaultColor
|
||||
)
|
||||
.setContentIntent(
|
||||
PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
Intent(this, MainActivity::class.java)
|
||||
.setAction(MainActivity.ACTION_UPDATES),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
else
|
||||
PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
)
|
||||
.setStyle(NotificationCompat.InboxStyle().applyHack {
|
||||
for (productItem in productItems.take(maxUpdates)) {
|
||||
val builder = SpannableStringBuilder(productItem.name)
|
||||
builder.setSpan(
|
||||
ForegroundColorSpan(Color.BLACK), 0, builder.length,
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
builder.append(' ').append(productItem.version)
|
||||
addLine(builder)
|
||||
}
|
||||
if (productItems.size > maxUpdates) {
|
||||
val summary =
|
||||
getString(R.string.plus_more_FORMAT, productItems.size - maxUpdates)
|
||||
if (Android.sdk(24)) {
|
||||
addLine(summary)
|
||||
} else {
|
||||
setSummaryText(summary)
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Job : JobService() {
|
||||
|
@ -180,4 +180,6 @@
|
||||
<string name="installed_applications">Installed applications</string>
|
||||
<string name="sort_filter">Sort & Filter</string>
|
||||
<string name="new_applications">New applications</string>
|
||||
<string name="sync_auto_install">Update apps after sync</string>
|
||||
<string name="sync_auto_install_summary">Automatically install app updates after syncing repositories</string>
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user