I tried to install the APK programmatically using PackageInstaller but it is failing without any error.
The session.commit() executing without any error and calls the callback Intent. But callback intent doesn't receive any extras.
#Throws(IOException::class)
fun installPackage(context: Context, `in`: InputStream, packageName: String): Boolean {
Log.i("install","installer called")
Log.i("install","in iteration")
val packageInstaller: PackageInstaller = context.getPackageManager().getPackageInstaller()
val params = SessionParams(
SessionParams.MODE_FULL_INSTALL
)
params.setAppPackageName(packageName)
// set params
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
Log.i("id",""+sessionId)
addApkToInstallSession("india.apk",session);
val intent = Intent(context, InstallResultReceiver::class.java)
intent.setAction("PACKAGE_INSTALLED")
val pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
Log.i("t",""+packageInstaller.getSessionInfo(sessionId).toString())
try {
session.commit(pendingIntent.intentSender)
}catch(e:Exception){
Log.i("",""+e.stackTrace)
}
Log.i("Down","install committed")
return true
}
#Throws(IOException::class)
private fun addApkToInstallSession(assetName: String, session: PackageInstaller.Session) {
session.openWrite("package", 0, -1).use { packageInSession ->
assets.open(assetName).use { `is` ->
val buffer = ByteArray(16384)
var n: Int
while (`is`.read(buffer).also { n = it } >= 0) {
packageInSession.write(buffer, 0, n)
}
}
}
}
You have to change the flags on PendingIntent.getBroadcast(). Try the following:
var flags = 0
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
flags = PendingIntent.FLAG_MUTABLE
}
val pendingIntent = PendingIntent.getBroadcast(
context,
sessionId,
intent,
flags
)
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")
}
}
}
}
I am using this package with customisation home_widget and trying to build a home widget with number buttons like this image.
I am trying to achieve it by the following code
My WidgetProvider.kt
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
// Open App on Widget Click
val pendingIntent = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
val counter = widgetData.getString("_number", "")
Log.i("onMethodCall", "onMethodCall: $counter")
print("onMethodCall: $counter")
var counterText = "$counter"
setTextViewText(R.id.tv_counter, counterText)
val pendingIntent2 = HomeWidgetLaunchIntent.getActivity(context,
MainActivity::class.java)
setOnClickPendingIntent(R.id.bt_update, pendingIntent2)
val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("myAppWidget://onNumberBtnClick"))
setOnClickPendingIntent(R.id.bt_one, backgroundIntent)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
I am passing intent as "onNumberBtnClick" and this is my broadcast function
fun getBroadcast(context: Context, uri: Uri? = null): PendingIntent {
val intent = Intent(context, HomeWidgetBackgroundReceiver::class.java)
intent.data = uri
intent.action = HOME_WIDGET_BACKGROUND_ACTION
var flags = PendingIntent.FLAG_UPDATE_CURRENT
if (Build.VERSION.SDK_INT >= 23) {
flags = flags or PendingIntent.FLAG_IMMUTABLE
}
return PendingIntent.getBroadcast(context, 0, intent, flags)
}
in my "HomeWidgetPlugin" class I have set that value in shared preference.
"onNumberBtnClick" -> {
if (call.hasArgument("id") && call.hasArgument("data")) {
Log.i("onMethodCallHere", "onMethodCallHere: ")
print("onMethodCallHere")
val id = call.argument<String>("id")
val data = call.argument<Any>("data")
val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit()
if(data != null) {
when (data) {
is Boolean -> prefs.putBoolean(id, data)
is Float -> prefs.putFloat(id, data)
is String -> prefs.putString(id, data)
is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data))
is Int -> prefs.putInt(id, data)
else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException())
}
} else {
prefs.remove(id);
}
result.success(prefs.commit())
}else {
result.error("-1", "InvalidArguments saveWidgetData must be called with id and data", IllegalArgumentException())
}
}
In main.dart I handled callback with workmanager plugin as in the example of the home widget plugin
Future<void> backgroundCallback(Uri? uri) async {
if (uri?.host == 'onNumberBtnClick') {
await HomeWidget.getWidgetData<String>('_number', defaultValue: "1").then((value) {
print("num:$value");
});
await HomeWidget.saveWidgetData<String>('_number', "13");
await HomeWidget.updateWidget(name: 'AppWidgetProvider', iOSName: 'AppWidgetProvider');
}
}
Can someone please help me with what I am missing or how can I achieve this?
I'm trying to build an auto updater via github. The app so far detects new available versions and downloads the apk file. I however cannot get it to install the apk file.
There are no crashes or Log messages indicating an error. The install dialog just does not show up.
This is my code:
fun downloadAndInstall(link: Uri, fileName: String){
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val downloadUri = link
val request = DownloadManager.Request(downloadUri)
request.setMimeType(MIME_TYPE)
val destination = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString() + "/" + fileName
val destinationUri = Uri.parse("file://$destination")
request.setDestinationUri(destinationUri)
fun showInstallOption(destination: String) {
val onComplete = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) {
val contentUri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + ".provider",
File(destination)
)
val installer = context.packageManager.packageInstaller
val resolver = context.contentResolver
resolver.openInputStream(contentUri)?.use { apkStream ->
val length =
DocumentFile.fromSingleUri(context, contentUri)?.length() ?: -1
val params =
PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = installer.createSession(params)
val session = installer.openSession(sessionId)
session.openWrite("INSTALL", 0, length).use { sessionStream ->
apkStream.copyTo(sessionStream)
session.fsync(sessionStream)
}
val intent = Intent(context, InstallReceiver::class.java)
intent.action = "com.blazecode.tsviewer.util.updater.SESSION_API_PACKAGE_INSTALLED"
val pi = PendingIntent.getBroadcast(
context,
3,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
session.commit(pi.intentSender)
session.close()
}
Toast.makeText(context, "done", Toast.LENGTH_LONG).show()
}
}
context.registerReceiver(onComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
}
showInstallOption(destination)
downloadManager.enqueue(request)
Toast.makeText(context, context.getString(R.string.downloading), Toast.LENGTH_LONG).show()
}
I tested this on Android 13 and 8, both with the same result. Android 13 is a physical device. Android 8 is an emulator.
I'm making an APK file download screen and i show progress which is the percent of the file downloaded in the screen and also i have a notification to use a Foreground service because the file download must be downloaded from a service. During downloading, users can leave the app and can go back to my app through the notification. But the problem is, to achieve this logic, i use PendingIntent and when user click my app's notification, it recreates the app instead of going back to the previous screen earlier. I don't know why. please check my codes below.
File Download Fragment
class FileDownloadFragment(private val uri: String) : DialogFragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
startDownloadService()
}
private fun startDownloadService() {
val downloadService = AppUpdateAPKDownloadService()
val startIntent = Intent(context, downloadService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(startIntent)
} else {
context.startService(startIntent)
}
}
...
}
Foreground Service
class AppUpdateAPKDownloadService: LifecycleService() {
...
/** Dispatcher */
private val ioDispatcher = Dispatchers.IO
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (intent != null) {
createNotificationChannel()
val notification = getNotification()
startForeground(NOTIFICATION_ID, notification)
lifecycleScope.launch{
downloadAPK(intent.getStringExtra(UPDATE_APK_URI).toString())
}
}
return START_NOT_STICKY
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableVibration(true)
enableLights(true)
}
val manager = this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
private fun getNotification(): Notification {
val intent = Intent(applicationContext, SplashActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
val pendingIntent = TaskStackBuilder.create(applicationContext).run {
addNextIntentWithParentStack(intent)
getPendingIntent(APP_UPDATE_PENDING_INTENT_REQUEST_CODE, PendingIntent.FLAG_IMMUTABLE)
}
// val pendingIntent = PendingIntent.getActivity(
// applicationContext,
// APP_UPDATE_PENDING_INTENT_REQUEST_CODE,
// intent,
// PendingIntent.FLAG_IMMUTABLE
// )
val notification = NotificationCompat.Builder(this, CHANNEL_ID).apply {
setContentTitle("DownloadApp")
setContentText("Downloading...")
setSmallIcon(R.drawable.icon)
setLargeIcon(BitmapFactory.decodeResource(this#AppUpdateAPKDownloadService.applicationContext.resources, R.mipmap.icon))
priority = NotificationCompat.PRIORITY_HIGH
setContentIntent(pendingIntent)
setOngoing(true)
setAutoCancel(true)
}.build()
val notificationManager = NotificationManagerCompat.from(this)
notificationManager.notify(NOTIFICATION_ID, notification)
return notification
}
private suspend fun downloadAPK(uri: String) = withContext(ioDispatcher) {
kotlin.runCatching {
URL(uri)
}.onSuccess { url ->
val connection: URLConnection = url.openConnection()
connection.connect()
val fileFullSize = connection.contentLength
val directory = applicationContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
directory?.let {
if (!directory.exists()) directory.mkdirs()
}
val inputDataStream = DataInputStream(BufferedInputStream(url.openStream(), fileFullSize))
val file = File(directory, "my_app.apk")
val outputDataStream = DataOutputStream(BufferedOutputStream(FileOutputStream(file), fileFullSize))
processDownload(
inputStream = inputDataStream,
outputStream = outputDataStream,
fileFullSize = fileFullSize
)
val bundle = Bundle().apply {
putInt(UPDATE_APK_RECEIVER_MODE, UPDATE_APK_DOWNLOAD_COMPLETE)
}
uiUpdateReceiver?.send(Activity.RESULT_OK, bundle)
}.onFailure {
}
}
private fun processDownload(
inputStream: DataInputStream,
outputStream: DataOutputStream,
fileFullSize: Int
) {
val data = ByteArray(fileFullSize)
var downloadSize: Long = 0
var count: Int
while (inputStream.read(data).also { count = it } != -1) {
downloadSize += count
val percent = (downloadSize.toFloat() / fileFullSize) * 100
Log.d("TEST", "writing...$percent% in Service")
val bundle = Bundle().apply {
putInt(UPDATE_APK_RECEIVER_MODE, UPDATE_APK_UI_UPDATE)
putFloat(UPDATE_API_UI_PERCENT, percent)
}
uiUpdateReceiver?.send(Activity.RESULT_OK, bundle)
outputStream.write(data, 0, count)
}
outputStream.flush()
outputStream.close()
inputStream.close()
}
...
}
I also add android:launchMode="singleTop" to SplashActivity in AndroidManifest.xml
but it's still not working...
What mistake did I make?
TaskStackBuilder will always recreate the activities. That's the way it is designed. You don't want to use it if you want to return to an existing task.
Instead of your code to create the PendingIntent, use this:
val intent = PackageManager.getLaunchIntentForPackage("your.package.name")
val pendingIntent = PendingIntent.getActivity(applicationContext,
APP_UPDATE_PENDING_INTENT_REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE)
This will just cause your existing task to be brought to the foreground in whatever state it was in last. If your app is not running, this will launch your app as it would if you tapped the app icon on the HOME screen.
Okay, I solved this problem. it's 3 steps.
use PendingIntent.getActivity() instead of TaskStackBuilder.create(applicationContext).
use PendingIntent.FLAG_IMMUTABLE if you target Android M(API23) or above, or PendingIntent.FLAG_UPDATE_CURRENT if you target Android Lollipop(API21) or below.
add these flags Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP to the Intent.
i am trying to open an apk file from within my activity the following way:
File.openAttachment(attachmentName: String, context: Context, activity: Activity, mimeType: String) {
if (attachmentName.isNotEmpty() && attachmentName != "-") {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.setDataAndType(this.getFileURI(), mimeType.toLowerCase(Locale.ENGLISH))
activity.startActivity(Intent.createChooser(intent, context.getString(R.string.choose_an_app)))
}
}
File(Utils.getAttachmentPathDirectory(MyApplication.applicationContext())+"/apkTest.apk")
.openAttachment("apkTest.apk", this, this, "application/vnd.android.package-archive")
i also tried the following way:
val intent_install = Intent(Intent.ACTION_VIEW)
intent_install.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent_install.setDataAndType(FileProvider.getUriForFile(mContext, mContext.packageName+".provider",File(Utils.getAttachmentPathDirectory(MyApplication.applicationContext())+"/"+fileName)), "application/vnd.android.package-archive")
mActivity?.startActivity(intent_install)
there are no errors but the file is not opening
any help is appreciated
thanks to #CommonWare and the provided link
https://gitlab.com/commonsguy/cw-android-q/tree/vFINAL/AppInstaller
i have managed to achieve what i want using the following code
val apkUri = FileProvider.getUriForFile(this, packageName+".provider", File(Utils.getAttachmentPathDirectory(MyApplication.applicationContext())+"/testapk.apk"))
MyApplication.applicationContext().contentResolver.openInputStream(apkUri).use { apkStream ->
val length = DocumentFile.fromSingleUri(application, apkUri)?.length() ?: -1
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
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(application, InstallReceiver::class.java)
val pi = PendingIntent.getBroadcast(
application,
PI_INSTALL,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
session.commit(pi.intentSender)
session.close()
}
class InstallReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -1)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val activityIntent =
intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
context.startActivity(activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}
PackageInstaller.STATUS_SUCCESS ->
ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100)
.startTone(ToneGenerator.TONE_PROP_ACK)
}
}
}