diff --git a/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt index a1cafb3e..20e63893 100644 --- a/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt +++ b/src/main/kotlin/com/looker/droidify/index/RepositoryUpdater.kt @@ -95,7 +95,13 @@ object RepositoryUpdater { repository: Repository, unstable: Boolean, callback: (Stage, Long, Long?) -> Unit ): Single { - 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( diff --git a/src/main/kotlin/com/looker/droidify/installer/AppInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/AppInstaller.kt new file mode 100644 index 00000000..fca683a6 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/AppInstaller.kt @@ -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 + } + } +} diff --git a/src/main/kotlin/com/looker/droidify/installer/BaseInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/BaseInstaller.kt new file mode 100644 index 00000000..b82ed559 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/BaseInstaller.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt new file mode 100644 index 00000000..571f6f0c --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/DefaultInstaller.kt @@ -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) + ) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/installer/InstallationEvents.kt b/src/main/kotlin/com/looker/droidify/installer/InstallationEvents.kt new file mode 100644 index 00000000..106da4c3 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/InstallationEvents.kt @@ -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) +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/installer/RootInstaller.kt b/src/main/kotlin/com/looker/droidify/installer/RootInstaller.kt new file mode 100644 index 00000000..fc3a3c21 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/installer/RootInstaller.kt @@ -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 "" + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt b/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt index 5a9dcdb9..7f521318 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ProductFragment.kt @@ -15,7 +15,6 @@ import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -24,20 +23,17 @@ import com.looker.droidify.R import com.looker.droidify.content.ProductPreferences import com.looker.droidify.database.Database import com.looker.droidify.entity.* +import com.looker.droidify.installer.AppInstaller import com.looker.droidify.service.Connection import com.looker.droidify.service.DownloadService import com.looker.droidify.utility.RxUtils 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.uninstallPackage import com.looker.droidify.utility.extension.android.* import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { companion object { @@ -385,10 +381,12 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { } (recyclerView?.adapter as? ProductAdapter)?.setStatus(status) if (state is DownloadService.State.Success && isResumed) { - lifecycleScope.launch(Dispatchers.IO) { - state.consume() - context?.startPackageInstaller(state.release.cacheFileName) - } + state.consume() + AppInstaller + .getInstance(context)?.defaultInstaller?.install( + "", + state.release.cacheFileName + ) } } @@ -433,11 +431,8 @@ class ProductFragment() : ScreenFragment(), ProductAdapter.Callbacks { ) } ProductAdapter.Action.UNINSTALL -> { - lifecycleScope.launch(Dispatchers.IO) { - this@ProductFragment.context?.uninstallPackage( - packageName - ) - } + AppInstaller.getInstance(context)?.defaultInstaller?.uninstall(packageName) + Unit } ProductAdapter.Action.CANCEL -> { val binder = downloadConnection.binder diff --git a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt index fe1935ad..f642f60f 100644 --- a/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt +++ b/src/main/kotlin/com/looker/droidify/screen/ScreenActivity.kt @@ -9,16 +9,13 @@ import android.widget.FrameLayout import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentActivity -import androidx.lifecycle.lifecycleScope 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.utility.KParcelable -import com.looker.droidify.utility.Utils.startPackageInstaller import com.looker.droidify.utility.extension.resources.getDrawableFromAttr import com.looker.droidify.utility.extension.text.nullIfEmpty -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch abstract class ScreenActivity : FragmentActivity() { companion object { @@ -218,8 +215,11 @@ abstract class ScreenActivity : FragmentActivity() { is SpecialIntent.Install -> { val packageName = specialIntent.packageName if (!packageName.isNullOrEmpty()) { - lifecycleScope.launch(Dispatchers.IO) { - specialIntent.cacheFileName?.let { startPackageInstaller(it) } + specialIntent.cacheFileName?.let { + AppInstaller + .getInstance( + this@ScreenActivity + )?.defaultInstaller?.install(packageName, it) } } Unit diff --git a/src/main/kotlin/com/looker/droidify/utility/Utils.kt b/src/main/kotlin/com/looker/droidify/utility/Utils.kt index ef10b71b..00ab11d3 100644 --- a/src/main/kotlin/com/looker/droidify/utility/Utils.kt +++ b/src/main/kotlin/com/looker/droidify/utility/Utils.kt @@ -1,14 +1,10 @@ package com.looker.droidify.utility import android.content.Context -import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.Signature import android.graphics.drawable.Drawable -import android.net.Uri -import android.util.Log 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 @@ -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.getDrawableCompat 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.cert.Certificate import java.security.cert.CertificateEncodingException @@ -90,58 +81,8 @@ object Utils { } } - suspend fun Context.startPackageInstaller(cacheFileName: String) { - val file = Cache.getReleaseFile(this, cacheFileName) - 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")) - ) - } - } + val rootInstallerEnabled: Boolean + get() = Preferences[Preferences.Key.RootPermission] fun startUpdate( packageName: String, @@ -171,43 +112,4 @@ object Utils { ) } 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 "" - } } diff --git a/src/main/kotlin/com/looker/droidify/utility/extension/Text.kt b/src/main/kotlin/com/looker/droidify/utility/extension/Text.kt index a2e86b35..a4657a36 100644 --- a/src/main/kotlin/com/looker/droidify/utility/extension/Text.kt +++ b/src/main/kotlin/com/looker/droidify/utility/extension/Text.kt @@ -35,7 +35,7 @@ fun String?.trimBefore(char: Char, repeated: Int): String? { this?.let { for (i in it.indices) { if (it[i] == char) count++ - if (repeated == count) return it.substring(i+1) + if (repeated == count) return it.substring(i + 1) } } return null diff --git a/src/main/res/layout/product_item.xml b/src/main/res/layout/product_item.xml index b2642d22..5d9d96d2 100644 --- a/src/main/res/layout/product_item.xml +++ b/src/main/res/layout/product_item.xml @@ -12,9 +12,9 @@ + android:orientation="horizontal" + android:paddingHorizontal="10dp">