I was working on an update for my app, a section of which deals with downloading and installing an APK file.
As long as the previous version were targeting SDK 30 everything worked pretty smoothly. But as soon as I incremented the target and compile SDK to 32 it just started behaving queerly.
Here is the code that deals which the package manager and the installation:
private fun installOnClickListener() {
binding.termuxInstallCard.showProgress()
var session: PackageInstaller.Session? = null
try {
val packageInstaller: PackageInstaller =
requireContext().packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL
)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
viewModel.addApkToSession(session)
var installBroadcast: PendingIntent? = null
val intent =
Intent(PACKAGE_INSTALLED_ACTION).putExtra(
"packageName",
"com.termux"
)
installBroadcast = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getBroadcast(
context,
0,
intent,
FLAG_MUTABLE
)
} else {
PendingIntent.getBroadcast(context, 0, intent, FLAG_UPDATE_CURRENT)
}
session.commit(installBroadcast.intentSender)
session.close()
} catch (e: IOException) {
throw RuntimeException("Couldn't install package", e)
} catch (e: RuntimeException) {
session?.abandon()
throw e
} finally {
session?.close()
}
}
Here is what is happening:
As I am targeting SDK 32, I am required to specify the Mutability of PendingIntent
Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent
When I use:
FLAG_MUTABLE- The installation just fails stating the error code- STATUS_FAILURE_INVALID with no extra message for debugging in EXTRA_STATUS_MESSAGE. The thing is that when I try to install the same downloaded APK via the adb shell, it just installs normally without any issues.
FLAG_IMMUTABLE- The installation succeeds without prompting user with the installation dialog but nothing is actually installed.
More code in case you need it-
fun addApkToInstallSession(
path: String,
session: PackageInstaller.Session
) {
val file = File("${context.filesDir.path}/$path")
val packageInSession: OutputStream = session.openWrite("com.termux", 0, -1)
val inputStream = FileInputStream(file)
val byteStream = inputStream.read()
try {
var c: Int
val buffer = ByteArray(16384)
while (inputStream.read(buffer).also { c = it } >= 0) {
packageInSession.write(buffer, 0, c)
}
} catch (e: IOException) {
println("IOEX")
} finally {
try {
packageInSession.close()
inputStream.close()
} catch (e: IOException) {
println("IOEX in closing the stream")
}
}
}
private val broadcastReceiverForInstallEvents = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
lifecycleScope.launch(Dispatchers.IO) {
val extras = intent.extras
val status = extras!!.getInt(PackageInstaller.EXTRA_STATUS)
val packageName = extras.getString("packageName")!!
if (PACKAGE_INSTALLED_ACTION == intent.action) {
println("STATUS $status")
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
try {
val confirmIntent = extras[Intent.EXTRA_INTENT] as Intent
confirmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(confirmIntent)
} catch (e: Exception) {
lifecycleScope.launch(Dispatchers.Main) {
Toast.makeText(
requireContext(),
"We could not find an application to handle the installation of apps. Please download a package installer.",
Toast.LENGTH_SHORT
).show()
}
}
}
PackageInstaller.STATUS_SUCCESS -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install succeeded!")
// todo all done animation
binding.termuxInstallCard.markAsComplete()
Toast.makeText(requireContext(), "All Done!", Toast.LENGTH_SHORT)
.show()
lifecycleScope.launch {
// viewModel.setTermuxSetupDone()
}
/* redirecting... */
Handler(Looper.getMainLooper()).postDelayed({
redirect()
}, 2000)
}
}
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
lifecycleScope.launch(Dispatchers.Main) {
println("Extra Status Message${extras.getString("EXTRA_STATUS_MESSAGE")}")
"There was an error installing Termux. Please retry.".showSnackbar(
binding.root,
true
)
binding.termuxInstallCard.hideProgress()
}
}
else -> {
lifecycleScope.launch(Dispatchers.Main) {
println("$packageName Install failed else!")
// exitActivity("Package failed to install -> Unknown Error!")
binding.termuxInstallCard.hideProgress()
}
}
}
}
}
}
}
I would really appreciate some help!
In the past when I tried to install APKs I used either Knox on Samsung devices or I'd refer the user to install the package via package manager, this is the code I've used:
public static void cleanInstall(String datum, Context context) {
File file = new File(datum);
if (file.exists()) {
Intent intent = new Intent(Intent.ACTION_VIEW);
String type = "application/vnd.android.package-archive";
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Uri downloadedApk = FileProvider.getUriForFile(context, ".MyPackage.myFileProvider", file);
intent.setDataAndType(downloadedApk, type);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
intent.setDataAndType(Uri.fromFile(file), type);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
context.startActivity(intent);
} else {
Toast.makeText(context, "ّFile not found!", Toast.LENGTH_SHORT).show();
}
}
I've not used this code in a while so I don't know if it works anymore but it's something you might want to check, to see if it does. Make sure the path you are referring to does not reside on SD as you'll probably face permission denied issues.
On calculating the hash of the file I was committing to the session of the Package Manager, I found out that it didn't write the APK file correctly.
val inputStream: InputStream = session.openRead("com.termux")
val file = File("${requireContext().filesDir.path}/test2.apk")
if (!file.exists()) {
file.createNewFile()
}
try {
inputStream.use { input ->
file.outputStream().use { output ->
input.copyTo(output)
}
}
} catch (e: Exception) {
println("Exception occured $e")
}
if (file.length() > 0) {
val hash = file.getMD5Hash()
println("HASH of session - $hash")
}
Fixed that and with the combination of the Mutable pending intent. The package is now installing perfectly.
Related
I can install the application without the play store with the apk I downloaded from the server, but I want to do this without asking for read/write permission.
In Firebase app tester we can install apk without getting read/write permissions. I want to do the same in my own application and I decided to use Package installer for this, but after the APK is downloaded, the update dialog appears on the screen and then I get the error below.
INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package com.xxx.xxx signatures do not match previously installed version; ignoring!
Soloution reference : https://commonsware.com/Q/pages/chap-pkg-001
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun installPackage(destination: String) {
val onComplete = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent1: Intent
) {
try {
context.registerReceiver(mInstallReceiver, IntentFilter(ACTION_INSTALL_COMPLETE))
factory.dismiss()
val installer = context.packageManager.packageInstaller
val resolver = context.contentResolver
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + PROVIDER_PATH,
File(destination)
)
resolver.openInputStream(contentUri)?.use { apkStream ->
val length = DocumentFile.fromSingleUri(context, contentUri)?.length() ?: -1
val params = SessionParams(SessionParams.MODE_FULL_INSTALL)
params.setAppPackageName(context.packageName)
val sessionId = installer.createSession(params)
val session = installer.openSession(sessionId)
session.openWrite(NAME, 0, length).use { sessionStream ->
apkStream.copyTo(sessionStream)
session.fsync(sessionStream)
}
val intent = Intent(ACTION_INSTALL_COMPLETE)
val pendingIntent = PendingIntent.getBroadcast(context, PI_INSTALL, intent, PendingIntent.FLAG_UPDATE_CURRENT)
session.commit(pendingIntent.intentSender)
session.close()
}
}
catch (e :Exception){
e.printStackTrace()
context.unregisterReceiver(mInstallReceiver)
}
}
}
context.registerReceiver(onComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
private val mInstallReceiver = object : BroadcastReceiver() {
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
override fun onReceive(context: Context, intent: Intent) {
if (ACTION_INSTALL_COMPLETE != intent.action) {
return
}
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val install = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
if (install?.resolveActivity(context.packageManager) != null) {
install.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(install)
}
}
PackageInstaller.STATUS_SUCCESS -> ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100)
.startTone(ToneGenerator.TONE_PROP_ACK)
else -> {
val msg = intent.getStringExtra(PackageInstaller.EXTRA_STATUS_MESSAGE)
context.showToast("$msg")
Log.e("Installer app", "received $status and $msg")
}
}
}
}
My app is a cloud storage on target SDK 30, I want to download folders in API 30 but it doesn't work.
At first, the user is directed to the SAF environment using ACTION_CREATE_DOCUMENT, after selecting the path, the files are downloaded in the App Data path and then copied to the path selected by the user.
fun openSavePickerForDirectories(activity: Activity) {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.type = DocumentsContract.Document.MIME_TYPE_DIR
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.putExtra(
Intent.EXTRA_TITLE, "Export folder"
)
try {
activity.startActivityForResult(
intent,
ErrorCodes.FILE_STORAGE_UTILS_OPEN_SAVE_PICKER_DIRECTORY
)
} catch (e: Exception) {
if (e.toString().contains("ActivityNotFoundException"))
activity.toast(activity.getString(R.string.filemanager_not_found))
e.printStackTrace()
}
}
#Throws(IOException::class)
fun copyDirectory(sourceLocation: File, targetLocation: File) {
if (sourceLocation.isDirectory) {
if (!targetLocation.exists() && !targetLocation.mkdirs()) {
throw IOException("Cannot create dir " + targetLocation.absolutePath)
}
val children = sourceLocation.list()
for (i in children.indices) {
copyDirectory(
File(sourceLocation, children[i]),
File(targetLocation, children[i])
)
}
} else {
copyFile(sourceLocation, targetLocation)
}
}
I am a beginner in android application development(Kotlin) and recently I was handover a project on NFT which involves walletConnect integration & for that I am using the walletConnectV1 library.
Fetching the public key and Connecting with metamask was not so hard but I am struggling when it comes to signing methods.
if anyone can help me with, how to sign messages and transactions or what I was doing wrong all this time that would really help me.
Thank you
Connect Button Click Listener
screen_main_connect_button.setOnClickListener {
try {
ExampleApplication.resetSession()
ExampleApplication.session.addCallback(this)
val i = Intent(Intent.ACTION_VIEW, Uri.parse(ExampleApplication.config.toWCUri()))
startActivity(i)
} catch (e: ActivityNotFoundException) {
// open play store
} catch (e: Exception) {
//handle exceptions
}
}
Response after the session was approved
private fun sessionApproved() {
uiScope.launch {
val account = session.approvedAccounts()?.get(0)?:""
screen_main_status.text = "Connected: $account"
screen_main_connect_button.visibility = View.GONE
screen_main_disconnect_button.visibility = View.VISIBLE
screen_main_tx_button.visibility = View.VISIBLE
val job = async {
personalSign(
"Sign this message of mine to this address",
account) {
Log.d(TAG, "sessionApproved: ${it.result}")
}
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("wc:")
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
}
}
}
private fun personalSign(
message: String,
address: String,
response: (Session.MethodCall.Response) -> Unit
) {
val id = System.currentTimeMillis()
val messageParam = if (message.hasHexPrefix()) message else message.toHex()
session.performMethodCall(
Session.MethodCall.Custom(
id, "personal_sign", listOf(messageParam, address)
)
) { response(it) }
}
I am trying to install an app from my fragment. I want the unknown source permission message to be displayed and then the installation process to take place. The problem is that on my first installation, app seems to be crashed.
Of course, next time (when installing another apk) this problem disappears. I did the following steps:
In my viewModel :
fun installApp(uri: Uri) {
viewModelScope.launch(context = exceptionHandler + DEFAULT) {
val promptInstall = Intent(Intent.ACTION_VIEW, uri)
promptInstall.putExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, true)
promptInstall.setDataAndType(uri, "application/vnd.android" + ".package-archive")
promptInstall.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
promptInstall.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivityMutableLiveData.postValue(promptInstall.toOneTimeEvent())
}
}
and then in my fragment :
viewModel.startActivityLiveData.observe(viewLifecycleOwner, Observer { oneTimeEvent ->
(oneTimeEvent.getValue() as Intent).startActivityFromIntent(requireActivity())})
And finally this is my extension function :
fun Intent.startActivityFromIntent(context: Context) = (context as Activity).startActivity(this)
Well, it seems that the process I mentioned above has a problem(it crashed!) in Android 10 and above. Here is the solution I found:
private fun Uri.installApp(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (!context.packageManager.canRequestPackageInstalls()) {
startForResult.launch(Intent(ACTION_MANAGE_UNKNOWN_APP_SOURCES))
Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES).setData(
} else {
viewModel.installApp(this)
}
} else {
viewModel.installApp(this)
}
and we must do as below :
private val startForResult =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
when (result.resultCode) {
RESULT_OK -> {
// your logic...
}
RESULT_CANCELED -> {
//do something
}
else -> {
}
}
}
From the app context? Do I have to include a permission?
And does it close/reboot the app?
Try this
private fun clearAppData() {
try {
// clearing app data
if (Build.VERSION_CODES.KITKAT <= Build.VERSION.SDK_INT) {
(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).clearApplicationUserData() // note: it has a return value!
} else {
val packageName = applicationContext.packageName
val runtime = Runtime.getRuntime()
runtime.exec("pm clear $packageName")
}
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
Note: This might kill/close your application after clearing user data