Fully Implement Silent Installation

Automated Code Formatting
This commit is contained in:
LooKeR 2021-10-13 00:32:34 +05:30
parent 8c8b8509a7
commit a8336bbde0
8 changed files with 432 additions and 378 deletions

View File

@ -5,21 +5,17 @@ import android.app.Application
import android.app.job.JobInfo import android.app.job.JobInfo
import android.app.job.JobScheduler import android.app.job.JobScheduler
import android.content.* import android.content.*
import android.content.pm.PackageInfo
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.content.Preferences import com.looker.droidify.content.Preferences
import com.looker.droidify.content.ProductPreferences import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.Database import com.looker.droidify.database.Database
import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.index.RepositoryUpdater import com.looker.droidify.index.RepositoryUpdater
import com.looker.droidify.network.Downloader import com.looker.droidify.network.Downloader
import com.looker.droidify.network.PicassoDownloader import com.looker.droidify.network.PicassoDownloader
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.SyncService import com.looker.droidify.service.SyncService
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils.toInstalledItem
import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.Android
import com.looker.droidify.utility.extension.android.singleSignature
import com.looker.droidify.utility.extension.android.versionCodeCompat
import com.squareup.picasso.OkHttp3Downloader import com.squareup.picasso.OkHttp3Downloader
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import java.net.InetSocketAddress import java.net.InetSocketAddress
@ -27,10 +23,6 @@ import java.net.Proxy
@Suppress("unused") @Suppress("unused")
class MainApplication : Application() { class MainApplication : Application() {
private fun PackageInfo.toInstalledItem(): InstalledItem {
val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty()
return InstalledItem(packageName, versionName.orEmpty(), versionCodeCompat, signatureString)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()

View File

@ -26,6 +26,8 @@ import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService import com.looker.droidify.service.DownloadService
import com.looker.droidify.utility.RxUtils import com.looker.droidify.utility.RxUtils
import com.looker.droidify.utility.Utils import com.looker.droidify.utility.Utils
import com.looker.droidify.utility.Utils.startPackageInstaller
import com.looker.droidify.utility.Utils.startUpdate
import com.looker.droidify.utility.extension.android.* import com.looker.droidify.utility.extension.android.*
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
@ -134,7 +136,6 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
} }
addOnScrollListener(scrollListener) addOnScrollListener(scrollListener)
addItemDecoration(adapter.gridItemDecoration) addItemDecoration(adapter.gridItemDecoration)
// addItemDecoration(DividerItemDecoration(context, adapter::configureDivider))
savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER) savedInstanceState?.getParcelable<ProductAdapter.SavedState>(STATE_ADAPTER)
?.let(adapter::restoreState) ?.let(adapter::restoreState)
layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER) layoutManagerState = savedInstanceState?.getParcelable(STATE_LAYOUT_MANAGER)
@ -385,27 +386,7 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
ProductAdapter.Action.INSTALL, ProductAdapter.Action.INSTALL,
ProductAdapter.Action.UPDATE -> { ProductAdapter.Action.UPDATE -> {
val installedItem = installed?.installedItem val installedItem = installed?.installedItem
val productRepository = Product.findSuggested(products, installedItem) { it.first } startUpdate(packageName, installedItem, products, downloadConnection)
val compatibleReleases = productRepository?.first?.selectedReleases.orEmpty()
.filter { installedItem == null || installedItem.signature == it.signature }
val release = if (compatibleReleases.size >= 2) {
compatibleReleases
.filter { it.platforms.contains(Android.primaryPlatform) }
.minByOrNull { it.platforms.size }
?: compatibleReleases.minByOrNull { it.platforms.size }
?: compatibleReleases.firstOrNull()
} else {
compatibleReleases.firstOrNull()
}
val binder = downloadConnection.binder
if (productRepository != null && release != null && binder != null) {
binder.enqueue(
packageName,
productRepository.first.name,
productRepository.second,
release
)
} else Unit
} }
ProductAdapter.Action.LAUNCH -> { ProductAdapter.Action.LAUNCH -> {
val launcherActivities = installed?.launcherActivities.orEmpty() val launcherActivities = installed?.launcherActivities.orEmpty()

View File

@ -1,7 +1,6 @@
package com.looker.droidify.screen package com.looker.droidify.screen
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import android.view.ViewGroup import android.view.ViewGroup
@ -11,15 +10,12 @@ import android.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Cache
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.utility.KParcelable import com.looker.droidify.utility.KParcelable
import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.Utils.startPackageInstaller
import com.looker.droidify.utility.extension.resources.getDrawableFromAttr import com.looker.droidify.utility.extension.resources.getDrawableFromAttr
import com.looker.droidify.utility.extension.text.nullIfEmpty import com.looker.droidify.utility.extension.text.nullIfEmpty
import com.topjohnwu.superuser.Shell
import java.io.File
abstract class ScreenActivity : FragmentActivity() { abstract class ScreenActivity : FragmentActivity() {
companion object { companion object {
@ -219,11 +215,7 @@ abstract class ScreenActivity : FragmentActivity() {
is SpecialIntent.Install -> { is SpecialIntent.Install -> {
val packageName = specialIntent.packageName val packageName = specialIntent.packageName
if (!packageName.isNullOrEmpty()) { if (!packageName.isNullOrEmpty()) {
val fragment = currentFragment specialIntent.cacheFileName?.let { startPackageInstaller(it) }
if (fragment !is ProductFragment || fragment.packageName != packageName) {
pushFragment(ProductFragment(packageName))
}
specialIntent.cacheFileName?.let(::startPackageInstaller)
} }
Unit Unit
} }
@ -244,37 +236,6 @@ abstract class ScreenActivity : FragmentActivity() {
} }
} }
internal fun startPackageInstaller(cacheFileName: String) {
val file = Cache.getReleaseFile(this, cacheFileName)
if (Preferences[Preferences.Key.RootPermission]) {
val commandBuilder = StringBuilder()
commandBuilder.append("settings put global verifier_verify_adb_installs 0 ; ")
commandBuilder.append(
getPackageInstallCommand(file)
)
commandBuilder.append(" ; settings put global verifier_verify_adb_installs 1")
Shell.su(commandBuilder.toString()).exec()
} else {
val (uri, flags) = if (Android.sdk(24)) {
Pair(
Cache.getReleaseUri(this, cacheFileName),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} else {
Pair(Uri.fromFile(file), 0)
}
// TODO Handle deprecation
@Suppress("DEPRECATION")
startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags)
)
}
}
private fun getPackageInstallCommand(cacheFile: File): String =
"cat \"${cacheFile.absolutePath}\" | pm install -t -r -S ${cacheFile.length()}"
internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName)) internal fun navigateProduct(packageName: String) = pushFragment(ProductFragment(packageName))
internal fun navigateRepositories() = pushFragment(RepositoriesFragment()) internal fun navigateRepositories() = pushFragment(RepositoriesFragment())
internal fun navigatePreferences() = pushFragment(SettingsFragment()) internal fun navigatePreferences() = pushFragment(SettingsFragment())

View File

@ -182,7 +182,9 @@ class SettingsFragment : ScreenFragment() {
preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled) preferences[Preferences.Key.ProxyHost]?.setEnabled(enabled)
preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled) preferences[Preferences.Key.ProxyPort]?.setEnabled(enabled)
} }
preferences[Preferences.Key.RootPermission]?.setEnabled(Shell.getShell().isRoot) preferences[Preferences.Key.RootPermission]?.setEnabled(
Shell.getCachedShell()?.isRoot ?: Shell.getShell().isRoot
)
if (key == Preferences.Key.Theme) { if (key == Preferences.Key.Theme) {
requireActivity().recreate() requireActivity().recreate()
} }

View File

@ -14,6 +14,7 @@ import com.looker.droidify.Common
import com.looker.droidify.MainActivity import com.looker.droidify.MainActivity
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Cache import com.looker.droidify.content.Cache
import com.looker.droidify.content.Preferences
import com.looker.droidify.entity.Release import com.looker.droidify.entity.Release
import com.looker.droidify.entity.Repository import com.looker.droidify.entity.Repository
import com.looker.droidify.network.Downloader import com.looker.droidify.network.Downloader
@ -300,9 +301,17 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
stateSubject.onNext(State.Success(task.packageName, task.name, task.release) { stateSubject.onNext(State.Success(task.packageName, task.name, task.release) {
consumed = true consumed = true
}) })
if (!consumed) { if (consumed || (Preferences[Preferences.Key.RootPermission])) {
showNotificationInstall(task) PendingIntent.getBroadcast(
} this,
0,
Intent(this, Receiver::class.java)
.setAction("$ACTION_INSTALL.${task.packageName}")
.putExtra(EXTRA_CACHE_FILE_NAME, task.release.cacheFileName),
PendingIntent.FLAG_UPDATE_CURRENT
)
.send()
} else showNotificationInstall(task)
} }
private fun validatePackage(task: Task, file: File): ValidationError? { private fun validatePackage(task: Task, file: File): ValidationError? {
@ -436,7 +445,7 @@ class DownloadService : ConnectionService<DownloadService.Binder>() {
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { result, throwable -> .subscribe { result, throwable ->
currentTask = null currentTask = null
throwable?.printStackTrace() throwable.printStackTrace()
if (result == null || !result.success) { if (result == null || !result.success) {
showNotificationError( showNotificationError(
task, task,

View File

@ -1,15 +1,31 @@
package com.looker.droidify.utility package com.looker.droidify.utility
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.Signature import android.content.pm.Signature
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri
import android.provider.Settings import android.provider.Settings
import android.util.Log
import com.looker.droidify.R import com.looker.droidify.R
import com.looker.droidify.content.Cache
import com.looker.droidify.content.Preferences
import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.entity.Product
import com.looker.droidify.entity.Repository
import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService
import com.looker.droidify.utility.extension.android.Android import com.looker.droidify.utility.extension.android.Android
import com.looker.droidify.utility.extension.android.singleSignature
import com.looker.droidify.utility.extension.android.versionCodeCompat
import com.looker.droidify.utility.extension.resources.getColorFromAttr import com.looker.droidify.utility.extension.resources.getColorFromAttr
import com.looker.droidify.utility.extension.resources.getDrawableCompat import com.looker.droidify.utility.extension.resources.getDrawableCompat
import com.looker.droidify.utility.extension.text.hex import com.looker.droidify.utility.extension.text.hex
import com.topjohnwu.superuser.Shell
import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.CertificateEncodingException import java.security.cert.CertificateEncodingException
@ -21,6 +37,11 @@ object Utils {
.apply { setTintList(context.getColorFromAttr(tintAttrResId)) } .apply { setTintList(context.getColorFromAttr(tintAttrResId)) }
} }
fun PackageInfo.toInstalledItem(): InstalledItem {
val signatureString = singleSignature?.let(Utils::calculateHash).orEmpty()
return InstalledItem(packageName, versionName.orEmpty(), versionCodeCompat, signatureString)
}
fun getDefaultApplicationIcons(context: Context): Pair<Drawable, Drawable> { fun getDefaultApplicationIcons(context: Context): Pair<Drawable, Drawable> {
val progressIcon: Drawable = val progressIcon: Drawable =
createDefaultApplicationIcon(context, android.R.attr.textColorSecondary) createDefaultApplicationIcon(context, android.R.attr.textColorSecondary)
@ -78,4 +99,92 @@ object Utils {
) != 0f ) != 0f
} }
} }
internal fun Activity.startPackageInstaller(cacheFileName: String) {
val file = Cache.getReleaseFile(this, cacheFileName)
if (Preferences[Preferences.Key.RootPermission]) {
val commandBuilder = StringBuilder()
val verifyState = getVerifyState()
if (verifyState == "1") commandBuilder.append("settings put global verifier_verify_adb_installs 0 ; ")
commandBuilder.append(getPackageInstallCommand(file))
commandBuilder.append(" ; settings put global verifier_verify_adb_installs $verifyState")
val result = Shell.su(commandBuilder.toString()).exec()
if (result.isSuccess) Shell.su("${getUtilBoxPath()} rm ${quote(file.absolutePath)}")
} else {
val (uri, flags) = if (Android.sdk(24)) {
Pair(
Cache.getReleaseUri(this, cacheFileName),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} else {
Pair(Uri.fromFile(file), 0)
}
// TODO Handle deprecation
@Suppress("DEPRECATION")
startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags)
)
}
}
fun startUpdate(
packageName: String,
installedItem: InstalledItem?,
products: List<Pair<Product, Repository>>,
downloadConnection: Connection<DownloadService.Binder, DownloadService>
) {
val productRepository = Product.findSuggested(products, installedItem) { it.first }
val compatibleReleases = productRepository?.first?.selectedReleases.orEmpty()
.filter { installedItem == null || installedItem.signature == it.signature }
val release = if (compatibleReleases.size >= 2) {
compatibleReleases
.filter { it.platforms.contains(Android.primaryPlatform) }
.minByOrNull { it.platforms.size }
?: compatibleReleases.minByOrNull { it.platforms.size }
?: compatibleReleases.firstOrNull()
} else {
compatibleReleases.firstOrNull()
}
val binder = downloadConnection.binder
if (productRepository != null && release != null && binder != null) {
binder.enqueue(
packageName,
productRepository.first.name,
productRepository.second,
release
)
} else Unit
}
private fun getPackageInstallCommand(cacheFile: File): String =
"cat \"${cacheFile.absolutePath}\" | pm install -t -r -S ${cacheFile.length()}"
private fun getVerifyState(): String =
Shell.sh("settings get global verifier_verify_adb_installs").exec().out[0]
private fun quote(string: String) =
"\"${string.replace(Regex("""[\\$"`]""")) { c -> "\\${c.value}" }}\""
private fun getUtilBoxPath(): String {
listOf("toybox", "busybox").forEach {
var shellResult = Shell.su("which $it").exec()
if (shellResult.out.isNotEmpty()) {
val utilBoxPath = shellResult.out.joinToString("")
if (utilBoxPath.isNotEmpty()) {
val utilBoxQuoted = quote(utilBoxPath)
shellResult = Shell.su("$utilBoxQuoted --version").exec()
if (shellResult.out.isNotEmpty()) {
val utilBoxVersion = shellResult.out.joinToString("")
Log.i(
this.javaClass.canonicalName,
"Using Utilbox $it : $utilBoxQuoted $utilBoxVersion"
)
}
return utilBoxQuoted
}
}
}
return ""
}
} }