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?
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 have a list view in my home screen widget and I want to launch a pending intent when the item is clicked. I referred to this and set a fillInIntent for every list item and then set a pending intent template which triggers the broadcast receiver of the widget:
class WidgetProvider : HomeWidgetProvider() {
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) {
appWidgetIds.forEach { widgetId ->
val views = RemoteViews(context.packageName, R.layout.widget_layout).apply {
val todosStr = widgetData.getString("todos", "null")
val todos = ArrayList<HashMap<String, Any>>()
val todosRemoteView = RemoteViews.RemoteCollectionItems.Builder()
if(todosStr != "null"){
val jObj = JSONObject(todosStr)
val jsonArry = jObj.getJSONArray("todos")
for (i in 0 until jsonArry.length()) {
val todo = HashMap<String, Any>()
val obj = jsonArry.getJSONObject(i)
todo["id"] = obj.getInt("id")
todos.add(todo)
val view = RemoteViews(context.packageName, R.layout.each_todo).apply {
setTextViewText(R.id.each_todo_container_text, todo["taskName"].toString())
val fillInIntent = Intent().apply {
Bundle().also { extras ->
extras.putInt("todo_id", todo["id"].toString().toInt())//this isn't working for some reason
putExtras(extras)
}
}
Log.d("debugging", "id received is ${todo["id"].toString()}" )
setOnClickFillInIntent(R.id.each_todo_container_text, fillInIntent)
}
todosRemoteView.addItem(todo["id"].toString().toInt().toLong(), view)
}
}
setRemoteAdapter(
R.id.todos_list,
todosRemoteView
.build()
)
val pendingIntentx: PendingIntent = Intent(
context,
WidgetProvider::class.java
).run {
PendingIntent.getBroadcast(context, 0, this, PendingIntent.FLAG_IMMUTABLE or Intent.FILL_IN_COMPONENT)
}
setPendingIntentTemplate(R.id.todos_list, pendingIntentx)
}
appWidgetManager.updateAppWidget(widgetId, views)
}
}
override fun onReceive(context: Context?, intent: Intent?) {
val viewIndex: Int = intent!!.getIntExtra("todo_id", 0)
Log.d("debugging", "an item is clicked $viewIndex")
//the default value gets used, which means that the receiver isn't receiving the intent data
super.onReceive(context, intent)
}
}
The problem is that the receiver isn't receiving the data which was put in the fillInIntent, the default value 0 is getting printed by the last statement. I also tried setting the FILL_IN_COMPONENT flag as suggested here, but that didn't work. Where am I going wrong?
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 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
)
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)
}
}
}