Add: New Installer Logic

This commit is contained in:
LooKeR 2021-10-24 18:03:35 +05:30
parent 8b3fd09f27
commit 2aaec7e022
11 changed files with 261 additions and 124 deletions

View File

@ -95,7 +95,13 @@ object RepositoryUpdater {
repository: Repository, unstable: Boolean, repository: Repository, unstable: Boolean,
callback: (Stage, Long, Long?) -> Unit callback: (Stage, Long, Long?) -> Unit
): Single<Boolean> { ): Single<Boolean> {
return update(context, repository, listOf(IndexType.INDEX_V1, IndexType.INDEX), unstable, callback) return update(
context,
repository,
listOf(IndexType.INDEX_V1, IndexType.INDEX),
unstable,
callback
)
} }
private fun update( private fun update(

View File

@ -0,0 +1,33 @@
package com.looker.droidify.installer
import android.content.Context
import com.looker.droidify.utility.Utils.rootInstallerEnabled
abstract class AppInstaller {
abstract val defaultInstaller: BaseInstaller?
companion object {
@Volatile
private var INSTANCE: AppInstaller? = null
fun getInstance(context: Context?): AppInstaller? {
if (INSTANCE == null) {
synchronized(AppInstaller::class.java) {
if (INSTANCE == null) {
context?.let {
INSTANCE = object : AppInstaller() {
override val defaultInstaller: BaseInstaller
get() {
return when (rootInstallerEnabled) {
false -> DefaultInstaller(it)
true -> RootInstaller(it)
}
}
}
}
}
}
}
return INSTANCE
}
}
}

View File

@ -0,0 +1,56 @@
package com.looker.droidify.installer
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.net.Uri
import com.looker.droidify.utility.extension.android.Android
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
abstract class BaseInstaller(val context: Context) : InstallationEvents {
companion object {
const val ROOT_INSTALL_PACKAGE = "cat %s | pm install --user %s -t -r -S %s"
const val ROOT_UNINSTALL_PACKAGE = "pm uninstall --user %s %s"
}
private val job = Job()
val scope = CoroutineScope(Dispatchers.IO + job)
fun getStatusString(context: Context, status: Int): String {
return when (status) {
PackageInstaller.STATUS_FAILURE -> "context.getString(R.string.installer_status_failure)"
PackageInstaller.STATUS_FAILURE_ABORTED -> "context.getString(R.string.installer_status_failure_aborted)"
PackageInstaller.STATUS_FAILURE_BLOCKED -> "context.getString(R.string.installer_status_failure_blocked)"
PackageInstaller.STATUS_FAILURE_CONFLICT -> "context.getString(R.string.installer_status_failure_conflict)"
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> "context.getString(R.string.installer_status_failure_incompatible)"
PackageInstaller.STATUS_FAILURE_INVALID -> "context.getString(R.string.installer_status_failure_invalid)"
PackageInstaller.STATUS_FAILURE_STORAGE -> "context.getString(R.string.installer_status_failure_storage)"
PackageInstaller.STATUS_PENDING_USER_ACTION -> "context.getString(R.string.installer_status_user_action)"
PackageInstaller.STATUS_SUCCESS -> "context.getString(R.string.installer_status_success)"
else -> "context.getString(R.string.installer_status_unknown)"
}
}
override fun uninstall(packageName: String) {
val uri = Uri.fromParts("package", packageName, null)
val intent = Intent()
intent.data = uri
if (Android.sdk(28)) {
intent.action = Intent.ACTION_DELETE
} else {
intent.action = Intent.ACTION_UNINSTALL_PACKAGE
intent.putExtra(Intent.EXTRA_RETURN_RESULT, true)
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
sealed class Event {
object INSTALL : Event()
object UNINSTALL : Event()
}
}

View File

@ -0,0 +1,43 @@
package com.looker.droidify.installer
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.looker.droidify.content.Cache
import com.looker.droidify.utility.extension.android.Android
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DefaultInstaller(context: Context) : BaseInstaller(context) {
override fun install(packageName: String, cacheFileName: String) {
val cacheFile = Cache.getReleaseFile(context, cacheFileName)
scope.launch { mDefaultInstaller(cacheFile) }
}
override fun install(packageName: String, cacheFile: File) {
scope.launch { mDefaultInstaller(cacheFile) }
}
private suspend fun mDefaultInstaller(cacheFile: File) {
val (uri, flags) = if (Android.sdk(24)) {
Pair(
Cache.getReleaseUri(context, cacheFile.name),
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
} else {
Pair(Uri.fromFile(cacheFile), 0)
}
// TODO Handle deprecation
@Suppress("DEPRECATION")
withContext(Dispatchers.IO) {
context.startActivity(
Intent(Intent.ACTION_INSTALL_PACKAGE)
.setDataAndType(uri, "application/vnd.android.package-archive").setFlags(flags)
)
}
}
}

View File

@ -0,0 +1,11 @@
package com.looker.droidify.installer
import java.io.File
interface InstallationEvents {
fun install(packageName: String, cacheFileName: String)
fun install(packageName: String, cacheFile: File)
fun uninstall(packageName: String)
}

View File

@ -0,0 +1,91 @@
package com.looker.droidify.installer
import android.content.Context
import android.util.Log
import com.looker.droidify.content.Cache
import com.looker.droidify.utility.Utils.rootInstallerEnabled
import com.looker.droidify.utility.extension.android.Android
import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class RootInstaller(context: Context) : BaseInstaller(context) {
override fun install(packageName: String, cacheFileName: String) {
val cacheFile = Cache.getReleaseFile(context, cacheFileName)
scope.launch { mRootInstaller(cacheFile) }
}
override fun install(packageName: String, cacheFile: File) {
scope.launch { mRootInstaller(cacheFile) }
}
override fun uninstall(packageName: String) {
scope.launch { mRootUninstaller(packageName) }
}
private suspend fun mRootInstaller(cacheFile: File) {
if (rootInstallerEnabled) {
val installCommand =
String.format(
ROOT_INSTALL_PACKAGE,
cacheFile.absolutePath,
getCurrentUserState,
cacheFile.length()
)
Log.e("Install", installCommand)
withContext(Dispatchers.IO) {
launch {
val result = Shell.su(installCommand).exec()
launch {
if (result.isSuccess) {
Shell.su("$getUtilBoxPath rm ${cacheFile.absolutePath.quote}")
.submit()
}
}
}
}
}
}
private suspend fun mRootUninstaller(packageName: String) {
if (rootInstallerEnabled) {
val uninstallCommand =
String.format(ROOT_UNINSTALL_PACKAGE, getCurrentUserState, packageName)
withContext(Dispatchers.IO) { launch { Shell.su(uninstallCommand).exec() } }
}
}
private val getCurrentUserState: String =
if (Android.sdk(25)) Shell.su("am get-current-user").exec().out[0]
else Shell.su("dumpsys activity | grep mCurrentUser").exec().out[0].trim()
.removePrefix("mCurrentUser=")
private val String.quote
get() = "\"${this.replace(Regex("""[\\$"`]""")) { c -> "\\${c.value}" }}\""
private val getUtilBoxPath: String
get() {
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 = utilBoxPath.quote
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 ""
}
}

View File

@ -15,7 +15,6 @@ import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -24,20 +23,17 @@ import com.looker.droidify.R
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.* import com.looker.droidify.entity.*
import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.service.Connection 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.Utils.startUpdate
import com.looker.droidify.utility.Utils.uninstallPackage
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
import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
companion object { companion object {
@ -385,10 +381,12 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
} }
(recyclerView?.adapter as? ProductAdapter)?.setStatus(status) (recyclerView?.adapter as? ProductAdapter)?.setStatus(status)
if (state is DownloadService.State.Success && isResumed) { if (state is DownloadService.State.Success && isResumed) {
lifecycleScope.launch(Dispatchers.IO) { state.consume()
state.consume() AppInstaller
context?.startPackageInstaller(state.release.cacheFileName) .getInstance(context)?.defaultInstaller?.install(
} "",
state.release.cacheFileName
)
} }
} }
@ -433,11 +431,8 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks {
) )
} }
ProductAdapter.Action.UNINSTALL -> { ProductAdapter.Action.UNINSTALL -> {
lifecycleScope.launch(Dispatchers.IO) { AppInstaller.getInstance(context)?.defaultInstaller?.uninstall(packageName)
this@ProductFragment.context?.uninstallPackage( Unit
packageName
)
}
} }
ProductAdapter.Action.CANCEL -> { ProductAdapter.Action.CANCEL -> {
val binder = downloadConnection.binder val binder = downloadConnection.binder

View File

@ -9,16 +9,13 @@ import android.widget.FrameLayout
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
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.utility.KParcelable import com.looker.droidify.utility.KParcelable
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
abstract class ScreenActivity : FragmentActivity() { abstract class ScreenActivity : FragmentActivity() {
companion object { companion object {
@ -218,8 +215,11 @@ abstract class ScreenActivity : FragmentActivity() {
is SpecialIntent.Install -> { is SpecialIntent.Install -> {
val packageName = specialIntent.packageName val packageName = specialIntent.packageName
if (!packageName.isNullOrEmpty()) { if (!packageName.isNullOrEmpty()) {
lifecycleScope.launch(Dispatchers.IO) { specialIntent.cacheFileName?.let {
specialIntent.cacheFileName?.let { startPackageInstaller(it) } AppInstaller
.getInstance(
this@ScreenActivity
)?.defaultInstaller?.install(packageName, it)
} }
} }
Unit Unit

View File

@ -1,14 +1,10 @@
package com.looker.droidify.utility package com.looker.droidify.utility
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageInfo 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.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.content.Preferences
import com.looker.droidify.entity.InstalledItem import com.looker.droidify.entity.InstalledItem
import com.looker.droidify.entity.Product import com.looker.droidify.entity.Product
@ -21,11 +17,6 @@ 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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
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
@ -90,58 +81,8 @@ object Utils {
} }
} }
suspend fun Context.startPackageInstaller(cacheFileName: String) { val rootInstallerEnabled: Boolean
val file = Cache.getReleaseFile(this, cacheFileName) get() = Preferences[Preferences.Key.RootPermission]
if (Preferences[Preferences.Key.RootPermission]) {
val commandBuilder = StringBuilder()
val verifyState = getVerifyState
val userId = getCurrentUserState()
if (verifyState == "1") commandBuilder.append("settings put global verifier_verify_adb_installs 0 ; ")
commandBuilder.append(getPackageInstallCommand(file, userId))
commandBuilder.append(" ; settings put global verifier_verify_adb_installs $verifyState")
withContext(Dispatchers.IO) {
launch {
val result = Shell.su(commandBuilder.toString()).exec()
launch {
if (result.isSuccess) {
Shell.su("${getUtilBoxPath()} rm ${quote(file.absolutePath)}").submit()
}
}
}
}
} 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)
)
}
}
suspend fun Context.uninstallPackage(packageName: String) {
if (Preferences[Preferences.Key.RootPermission]) {
val commandBuilder = StringBuilder()
val userId = getCurrentUserState()
commandBuilder.append(getPackageUninstallCommand(packageName, userId))
withContext(Dispatchers.IO) { launch { Shell.su(commandBuilder.toString()).exec() } }
} else {
// TODO Handle deprecation
@Suppress("DEPRECATION")
startActivity(
Intent(Intent.ACTION_UNINSTALL_PACKAGE)
.setData(Uri.parse("package:$packageName"))
)
}
}
fun startUpdate( fun startUpdate(
packageName: String, packageName: String,
@ -171,43 +112,4 @@ object Utils {
) )
} else Unit } else Unit
} }
private fun getPackageInstallCommand(cacheFile: File, userId: String = "0"): String =
"cat \"${cacheFile.absolutePath}\" | pm install --user $userId -t -r -S ${cacheFile.length()}"
private fun getPackageUninstallCommand(packageName: String, userId: String = "0"): String =
"cat \"$packageName\" | pm uninstall --user $userId $packageName"
private fun getCurrentUserState(): String =
if (Android.sdk(25)) Shell.su("am get-current-user").exec().out[0]
else Shell.su("dumpsys activity | grep mCurrentUser").exec().out[0].trim()
.removePrefix("mCurrentUser=")
private val 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 ""
}
} }

View File

@ -35,7 +35,7 @@ fun String?.trimBefore(char: Char, repeated: Int): String? {
this?.let { this?.let {
for (i in it.indices) { for (i in it.indices) {
if (it[i] == char) count++ if (it[i] == char) count++
if (repeated == count) return it.substring(i+1) if (repeated == count) return it.substring(i + 1)
} }
} }
return null return null

View File

@ -12,9 +12,9 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingHorizontal="10dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal"> android:orientation="horizontal"
android:paddingHorizontal="10dp">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/icon" android:id="@+id/icon"