Warning, /network/kdeconnect-android/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetProvider.kt is written in an unsupported language. File is not indexed.

0001 /*
0002  * SPDX-FileCopyrightText: 2023 Albert Vaca Cintora <albertvaka@gmail.com>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 package org.kde.kdeconnect.Plugins.RunCommandPlugin
0008 
0009 import android.app.PendingIntent
0010 import android.appwidget.AppWidgetManager
0011 import android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID
0012 import android.appwidget.AppWidgetProvider
0013 import android.content.ComponentName
0014 import android.content.Context
0015 import android.content.Intent
0016 import android.net.Uri
0017 import android.util.Log
0018 import android.widget.RemoteViews
0019 import org.kde.kdeconnect.Device
0020 import org.kde.kdeconnect.KdeConnect
0021 import org.kde.kdeconnect_tp.BuildConfig
0022 import org.kde.kdeconnect_tp.R
0023 
0024 const val RUN_COMMAND_ACTION = "RUN_COMMAND_ACTION"
0025 const val TARGET_COMMAND = "TARGET_COMMAND"
0026 const val TARGET_DEVICE = "TARGET_DEVICE"
0027 
0028 class RunCommandWidgetProvider : AppWidgetProvider() {
0029 
0030     override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
0031         for (appWidgetId in appWidgetIds) {
0032             updateAppWidget(context, appWidgetManager, appWidgetId)
0033         }
0034     }
0035 
0036     override fun onDeleted(context: Context, appWidgetIds: IntArray) {
0037         for (appWidgetId in appWidgetIds) {
0038             deleteWidgetDeviceIdPref(context, appWidgetId)
0039         }
0040     }
0041 
0042     override fun onEnabled(context: Context) {
0043         super.onEnabled(context)
0044         KdeConnect.getInstance().addDeviceListChangedCallback("RunCommandWidget") {
0045             forceRefreshWidgets(context)
0046         }
0047     }
0048 
0049     override fun onDisabled(context: Context) {
0050         KdeConnect.getInstance().removeDeviceListChangedCallback("RunCommandWidget")
0051         super.onDisabled(context)
0052     }
0053 
0054     override fun onReceive(context: Context, intent: Intent) {
0055         Log.d("WidgetProvider", "onReceive " + intent.action)
0056 
0057         if (intent.action == RUN_COMMAND_ACTION) {
0058             val targetCommand = intent.getStringExtra(TARGET_COMMAND)
0059             val targetDevice = intent.getStringExtra(TARGET_DEVICE)
0060             val plugin = KdeConnect.getInstance().getDevicePlugin(targetDevice, RunCommandPlugin::class.java)
0061             if (plugin != null) {
0062                 try {
0063                     plugin.runCommand(targetCommand)
0064                 } catch (ex: Exception) {
0065                     Log.e("RunCommandWidget", "Error running command", ex)
0066                 }
0067             } else {
0068                 Log.w("RunCommandWidget", "Device not available or runcommand plugin disabled");
0069             }
0070         } else {
0071             super.onReceive(context, intent);
0072         }
0073     }
0074 }
0075 
0076 fun getAllWidgetIds(context : Context) : IntArray {
0077     return AppWidgetManager.getInstance(context).getAppWidgetIds(
0078         ComponentName(context, RunCommandWidgetProvider::class.java)
0079     )
0080 }
0081 
0082 fun forceRefreshWidgets(context : Context) {
0083     val intent = Intent(context, RunCommandWidgetProvider::class.java)
0084     intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
0085     intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, getAllWidgetIds(context))
0086     context.sendBroadcast(intent)
0087 }
0088 
0089 /**
0090  * Recreate the [RemoteViews] layout of a given widget.
0091  *
0092  * This function is called when a new widget is created, or when the list of devices changes, or if
0093  * a device enables/disables its [RunCommandPlugin]. Hosting apps that contain our widgets will do
0094  * anything they can to avoid extra renders.
0095  *
0096  * 1. We use [appWidgetId] as a request code in [assignTitleIntent] to force hosting apps to track a
0097  *    separate intent for each widget.
0098  * 2. We call [AppWidgetManager.notifyAppWidgetViewDataChanged] at the end of this function, which
0099  *    lets the list adapter know that it might be referring to the wrong device id.
0100  *
0101  * See also [RunCommandWidgetDataProvider.onDataSetChanged].
0102  */
0103 internal fun updateAppWidget(
0104     context: Context,
0105     appWidgetManager: AppWidgetManager,
0106     appWidgetId: Int
0107 ) {
0108     Log.d("WidgetProvider", "updateAppWidget: $appWidgetId")
0109 
0110     // Determine which device provided these commands
0111     val deviceId = loadWidgetDeviceIdPref(context, appWidgetId)
0112     val device: Device? = if (deviceId != null) KdeConnect.getInstance().getDevice(deviceId) else null
0113 
0114     val views = RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_remotecommandplugin)
0115     assignTitleIntent(context, appWidgetId, views)
0116 
0117     Log.d("WidgetProvider", "updateAppWidget device: " + if (device == null) "null" else device.name)
0118 
0119     // Android should automatically toggle between the command list and the error text
0120     views.setEmptyView(R.id.widget_command_list, R.id.widget_error_text)
0121 
0122     // TODO: Use string resources
0123 
0124     if (device == null) {
0125         // There are two reasons we reach this condition:
0126         // 1. there is no preference string for this widget id
0127         // 2. the string id does not match any devices in KdeConnect.getInstance()
0128         // In both cases, we want the user to assign a device to this widget
0129         views.setTextViewText(R.id.widget_title_text, context.getString(R.string.kde_connect))
0130         views.setTextViewText(R.id.widget_error_text, "Whose commands should we show? Click the title to set a device.")
0131     } else {
0132         views.setTextViewText(R.id.widget_title_text, device.name)
0133         val plugin = device.getPlugin(RunCommandPlugin::class.java)
0134         if (device.isReachable) {
0135             val message: String = if (plugin == null) {
0136                 "Device doesn't allow us to run commands."
0137             } else {
0138                 "Device has no commands available."
0139             }
0140             views.setTextViewText(R.id.widget_error_text, message)
0141             assignListAdapter(context, appWidgetId, views)
0142             assignListIntent(context, appWidgetId, views)
0143         } else {
0144             views.setTextViewText(R.id.widget_error_text, context.getString(R.string.runcommand_notreachable))
0145         }
0146     }
0147 
0148     appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_command_list)
0149     appWidgetManager.updateAppWidget(appWidgetId, views)
0150 }
0151 
0152 /**
0153  * Create an Intent to launch the config activity whenever the title is clicked.
0154  *
0155  * See [RunCommandWidgetConfigActivity].
0156  */
0157 private fun assignTitleIntent(context: Context, appWidgetId: Int, views: RemoteViews) {
0158     val setDeviceIntent = Intent(context, RunCommandWidgetConfigActivity::class.java)
0159     setDeviceIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
0160     // We pass appWidgetId as requestCode even if it's not used to force the creation a new PendingIntent
0161     // instead of reusing an existing one, which is what happens if only the "extras" field differs.
0162     // Docs: https://developer.android.com/reference/android/app/PendingIntent.html
0163     val setDevicePendingIntent = PendingIntent.getActivity(context, appWidgetId, setDeviceIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
0164     views.setOnClickPendingIntent(R.id.widget_title_wrapper, setDevicePendingIntent)
0165 }
0166 
0167 /**
0168  * Configure remote adapter
0169  *
0170  * This function can only be called once in the lifetime of the widget. Subsequent calls do nothing.
0171  * Use [RunCommandWidgetConfigActivity] and the config function [saveWidgetDeviceIdPref] to change
0172  * the adapter's behavior.
0173  */
0174 private fun assignListAdapter(context: Context, appWidgetId: Int, views: RemoteViews) {
0175     val dataProviderIntent = Intent(context, CommandsRemoteViewsService::class.java)
0176     dataProviderIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
0177     dataProviderIntent.data = Uri.parse(dataProviderIntent.toUri(Intent.URI_INTENT_SCHEME))
0178     views.setRemoteAdapter(R.id.widget_command_list, dataProviderIntent)
0179 }
0180 
0181 /**
0182  * This pending intent allows the remote adapter to call fillInIntent so list items can do things.
0183  *
0184  * See [RemoteViews.setOnClickFillInIntent].
0185  */
0186 private fun assignListIntent(context: Context, appWidgetId: Int, views: RemoteViews) {
0187     val runCommandTemplateIntent = Intent(context, RunCommandWidgetProvider::class.java)
0188     runCommandTemplateIntent.action = RUN_COMMAND_ACTION
0189     runCommandTemplateIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId)
0190     val runCommandTemplatePendingIntent = PendingIntent.getBroadcast(context, appWidgetId, runCommandTemplateIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE)
0191     views.setPendingIntentTemplate(R.id.widget_command_list, runCommandTemplatePendingIntent)
0192 }