From 548b636f325003e1b97f987bcf8926ea722ec66c Mon Sep 17 00:00:00 2001 From: Philip Cohn-Cort Date: Mon, 23 Oct 2023 21:58:08 +0000 Subject: [PATCH] Better device controls ## Summary This contains some minor code quality improvements in `RunCommandControlsProviderService`, as well as the following feature changes: * If the device for a Device Control is reachable, clicking on the secondary space of the control will launch RunCommandActivity. If the device isn't reachable, we launch the MainActivity like usual. * Pixel 7 and other modern Google devices can now show KDE Connect commands in the 'Home' quick access tile (you still have to 'add app') ## Test Plan 0. Make sure your Android OS supports Device Controls (Android 11+) 1. Choose a paired device to work with 2. Place at least one command in the Run Command list 3. Enable the command in the Device Controls screen 4. Test what happens when you click on the secondary space of the control --- AndroidManifest.xml | 3 +- res/layout/activity_run_command.xml | 2 +- res/layout/toolbar.xml | 4 +- res/layout/widget_remotecommandplugin.xml | 9 +- res/values/strings.xml | 7 ++ res/xml/runcommand_preferences.xml | 32 +++++ .../RunCommandPlugin/CommandEntry.java | 8 ++ .../RunCommandPlugin/RunCommandActivity.java | 40 +++--- .../RunCommandControlsProviderService.kt | 56 +++++---- .../RunCommandPlugin/RunCommandPlugin.java | 31 +++-- .../RunCommandWidgetDataProvider.kt | 17 ++- .../RunCommandWidgetProvider.kt | 119 +++++++++++++----- 12 files changed, 227 insertions(+), 101 deletions(-) create mode 100644 res/xml/runcommand_preferences.xml diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 7c759a25..ab737ba8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -53,7 +53,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted diff --git a/res/layout/activity_run_command.xml b/res/layout/activity_run_command.xml index 5910247c..1f6955ff 100644 --- a/res/layout/activity_run_command.xml +++ b/res/layout/activity_run_command.xml @@ -43,7 +43,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted app:layout_anchorGravity="bottom|end" /> + tools:title="@string/kde_connect"/> diff --git a/res/layout/widget_remotecommandplugin.xml b/res/layout/widget_remotecommandplugin.xml index 5e39411a..e6355bad 100644 --- a/res/layout/widget_remotecommandplugin.xml +++ b/res/layout/widget_remotecommandplugin.xml @@ -16,13 +16,14 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted tools:ignore="RtlSymmetry"> Stop the current player Copy URL to clipboard Copied to clipboard + Device is not reachable Device is not paired There is no such device This device does not have the Run Command Plugin enabled + Device Controls + runcommand_category_device_controls + If your device supports Device Controls, commands you have configured will appear there. + set_runcommand_name_as_title + Show name as title + Find remote device Ring your remote device Ring diff --git a/res/xml/runcommand_preferences.xml b/res/xml/runcommand_preferences.xml new file mode 100644 index 00000000..b72e9de2 --- /dev/null +++ b/res/xml/runcommand_preferences.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java index 2dab2572..07c52f8a 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java @@ -6,11 +6,19 @@ package org.kde.kdeconnect.Plugins.RunCommandPlugin; +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; import org.kde.kdeconnect.UserInterface.List.EntryItem; class CommandEntry extends EntryItem { private final String key; + public CommandEntry(@NonNull JSONObject o) throws JSONException { + this(o.getString("name"), o.getString("command"), o.getString("key")); + } + public CommandEntry(String name, String cmd, String key) { super(name, cmd); this.key = key; diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java index df37fe4b..4ff50eff 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandActivity.java @@ -23,6 +23,7 @@ import androidx.core.content.ContextCompat; import org.json.JSONException; import org.json.JSONObject; +import org.kde.kdeconnect.Device; import org.kde.kdeconnect.KdeConnect; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect_tp.R; @@ -52,8 +53,7 @@ public class RunCommandActivity extends AppCompatActivity { commandItems = new ArrayList<>(); for (JSONObject obj : plugin.getCommandList()) { try { - commandItems.add(new CommandEntry(obj.getString("name"), - obj.getString("command"), obj.getString("key"))); + commandItems.add(new CommandEntry(obj)); } catch (JSONException e) { Log.e("RunCommand", "Error parsing JSON", e); } @@ -71,8 +71,8 @@ public class RunCommandActivity extends AppCompatActivity { if (!plugin.canAddCommand()) { text += "\n" + getString(R.string.addcommand_explanation2); } - binding.addComandExplanation.setText(text); - binding.addComandExplanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE); + binding.addCommandExplanation.setText(text); + binding.addCommandExplanation.setVisibility(commandItems.isEmpty() ? View.VISIBLE : View.GONE); } @Override @@ -87,21 +87,25 @@ public class RunCommandActivity extends AppCompatActivity { getSupportActionBar().setDisplayShowHomeEnabled(true); deviceId = getIntent().getStringExtra("deviceId"); - RunCommandPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId,RunCommandPlugin.class); - if (plugin != null) { - if (plugin.canAddCommand()) { - binding.addCommandButton.show(); - } else { - binding.addCommandButton.hide(); + Device device = KdeConnect.getInstance().getDevice(deviceId); + if (device != null) { + getSupportActionBar().setSubtitle(device.getName()); + RunCommandPlugin plugin = device.getPlugin(RunCommandPlugin.class); + if (plugin != null) { + if (plugin.canAddCommand()) { + binding.addCommandButton.show(); + } else { + binding.addCommandButton.hide(); + } + binding.addCommandButton.setOnClickListener(v -> { + plugin.sendSetupPacket(); + new AlertDialog.Builder(RunCommandActivity.this) + .setTitle(R.string.add_command) + .setMessage(R.string.add_command_description) + .setPositiveButton(R.string.ok, null) + .show(); + }); } - binding.addCommandButton.setOnClickListener(v -> { - plugin.sendSetupPacket(); - new AlertDialog.Builder(RunCommandActivity.this) - .setTitle(R.string.add_command) - .setMessage(R.string.add_command_description) - .setPositiveButton(R.string.ok, null) - .show(); - }); } updateView(); } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandControlsProviderService.kt b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandControlsProviderService.kt index 73cb74b8..43061d60 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandControlsProviderService.kt +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandControlsProviderService.kt @@ -13,6 +13,7 @@ import android.graphics.drawable.Icon import android.os.Build import android.service.controls.Control import android.service.controls.ControlsProviderService +import android.service.controls.DeviceTypes import android.service.controls.actions.CommandAction import android.service.controls.actions.ControlAction import android.service.controls.templates.StatelessTemplate @@ -23,16 +24,16 @@ import io.reactivex.Flowable import io.reactivex.processors.ReplayProcessor import org.json.JSONArray import org.json.JSONException +import org.json.JSONObject import org.kde.kdeconnect.Device import org.kde.kdeconnect.KdeConnect import org.kde.kdeconnect.UserInterface.MainActivity import org.kde.kdeconnect_tp.R import org.reactivestreams.FlowAdapters -import java.util.* import java.util.concurrent.Flow import java.util.function.Consumer -private class CommandEntryWithDevice(name: String, cmd: String, key: String, val device: Device) : CommandEntry(name, cmd, key) +private class CommandEntryWithDevice(o: JSONObject, val device: Device) : CommandEntry(o) @RequiresApi(Build.VERSION_CODES.R) class RunCommandControlsProviderService : ControlsProviderService() { @@ -56,23 +57,13 @@ class RunCommandControlsProviderService : ControlsProviderService() { for (controlId in controlIds) { val commandEntry = getCommandByControlId(controlId) if (commandEntry != null && commandEntry.device.isReachable) { - updatePublisher.onNext(Control.StatefulBuilder(controlId, getIntent(commandEntry.device)) - .setTitle(commandEntry.name) - .setSubtitle(commandEntry.command) - .setStructure(commandEntry.device.name) + updatePublisher.onNext(createStatefulBuilder(commandEntry, controlId) .setStatus(Control.STATUS_OK) .setStatusText(getString(R.string.tap_to_execute)) - .setControlTemplate(StatelessTemplate(commandEntry.key)) - .setCustomIcon(Icon.createWithResource(this, R.drawable.run_command_plugin_icon_24dp)) .build()) } else if (commandEntry != null && commandEntry.device.isPaired && !commandEntry.device.isReachable) { - updatePublisher.onNext(Control.StatefulBuilder(controlId, getIntent(commandEntry.device)) - .setTitle(commandEntry.name) - .setSubtitle(commandEntry.command) - .setStructure(commandEntry.device.name) + updatePublisher.onNext(createStatefulBuilder(commandEntry, controlId) .setStatus(Control.STATUS_DISABLED) - .setControlTemplate(StatelessTemplate(commandEntry.key)) - .setCustomIcon(Icon.createWithResource(this, R.drawable.run_command_plugin_icon_24dp)) .build()) } else { updatePublisher.onNext(Control.StatefulBuilder(controlId, getIntent(commandEntry?.device)) @@ -101,14 +92,9 @@ class RunCommandControlsProviderService : ControlsProviderService() { consumer.accept(ControlAction.RESPONSE_FAIL) } - updatePublisher.onNext(Control.StatefulBuilder(controlId, getIntent(commandEntry.device)) - .setTitle(commandEntry.name) - .setSubtitle(commandEntry.command) - .setStructure(commandEntry.device.name) + updatePublisher.onNext(createStatefulBuilder(commandEntry, controlId) .setStatus(Control.STATUS_OK) .setStatusText(getString(R.string.tap_to_execute)) - .setControlTemplate(StatelessTemplate(commandEntry.key)) - .setCustomIcon(Icon.createWithResource(this, R.drawable.run_command_plugin_icon_24dp)) .build()) } } @@ -126,7 +112,7 @@ class RunCommandControlsProviderService : ControlsProviderService() { for (index in 0 until jsonArray.length()) { val jsonObject = jsonArray.getJSONObject(index) - commandList.add(CommandEntryWithDevice(jsonObject.getString("name"), jsonObject.getString("command"), jsonObject.getString("key"), device)) + commandList.add(CommandEntryWithDevice(jsonObject, device)) } commandList @@ -151,7 +137,7 @@ class RunCommandControlsProviderService : ControlsProviderService() { if (plugin != null) { for (jsonObject in plugin.commandList) { try { - commandList.add(CommandEntryWithDevice(jsonObject.getString("name"), jsonObject.getString("command"), jsonObject.getString("key"), device)) + commandList.add(CommandEntryWithDevice(jsonObject, device)) } catch (error: JSONException) { Log.e("RunCommand", "Error parsing JSON", error) } @@ -171,7 +157,7 @@ class RunCommandControlsProviderService : ControlsProviderService() { val commandList = if (device.isReachable) { device.getPlugin(RunCommandPlugin::class.java)?.commandList?.map { jsonObject -> - CommandEntryWithDevice(jsonObject.getString("name"), jsonObject.getString("command"), jsonObject.getString("key"), device) + CommandEntryWithDevice(jsonObject, device) } } else { getSavedCommandsList(device) @@ -187,9 +173,29 @@ class RunCommandControlsProviderService : ControlsProviderService() { } } + private fun createStatefulBuilder(commandEntry: CommandEntryWithDevice, controlId: String): Control.StatefulBuilder { + if (!this::sharedPreferences.isInitialized) { + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + } + val useNameForTitle = sharedPreferences.getBoolean(getString(R.string.set_runcommand_name_as_title), true) + + return Control.StatefulBuilder(controlId, getIntent(commandEntry.device)) + .setTitle(if (useNameForTitle) commandEntry.name else commandEntry.command) + .setSubtitle(if (useNameForTitle) commandEntry.command else commandEntry.name) + .setStructure(commandEntry.device.name) + .setControlTemplate(StatelessTemplate(commandEntry.key)) + .setDeviceType(DeviceTypes.TYPE_ROUTINE) + .setCustomIcon(Icon.createWithResource(this, R.drawable.run_command_plugin_icon_24dp)) + } + private fun getIntent(device: Device?): PendingIntent { - val intent = Intent(Intent.ACTION_MAIN).setClass(this, MainActivity::class.java).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.putExtra(MainActivity.EXTRA_DEVICE_ID, device?.deviceId) + val target = if (device?.isReachable == true) RunCommandActivity::class else MainActivity::class + + val intent = Intent(Intent.ACTION_MAIN) + .setClass(this, target.java) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(MainActivity.EXTRA_DEVICE_ID, device?.deviceId) + return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) } } \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java index e10ed648..ab59d81f 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java @@ -18,20 +18,22 @@ import android.util.Log; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.preference.PreferenceManager; +import org.apache.commons.collections4.iterators.IteratorIterable; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Iterator; @PluginFactory.LoadablePlugin public class RunCommandPlugin extends Plugin { @@ -78,6 +80,17 @@ public class RunCommandPlugin extends Plugin { return context.getResources().getString(R.string.pref_plugin_runcommand_desc); } + @Override + public boolean hasSettings() { + return true; + } + + @Nullable + @Override + public PluginSettingsFragment getSettingsFragment(Activity activity) { + return PluginSettingsFragment.newInstance(getPluginKey(), R.xml.runcommand_preferences); + } + @Override public @DrawableRes int getIcon() { return R.drawable.run_command_plugin_icon_24dp; @@ -98,20 +111,14 @@ public class RunCommandPlugin extends Plugin { try { commandItems.clear(); JSONObject obj = new JSONObject(np.getString("commandList")); - Iterator keys = obj.keys(); - while (keys.hasNext()) { - String s = keys.next(); + for (String s : new IteratorIterable<>(obj.keys())) { JSONObject o = obj.getJSONObject(s); o.put("key", s); commandList.add(o); try { commandItems.add( - new CommandEntry( - o.getString("name"), - o.getString("command"), - o.getString("key") - ) + new CommandEntry(o) ); } catch (JSONException e) { Log.e("RunCommand", "Error parsing JSON", e); @@ -128,7 +135,9 @@ public class RunCommandPlugin extends Plugin { array.put(command); } - sharedPreferences.edit().putString(KEY_COMMANDS_PREFERENCE + device.getDeviceId(), array.toString()).apply(); + sharedPreferences.edit() + .putString(KEY_COMMANDS_PREFERENCE + device.getDeviceId(), array.toString()) + .apply(); } forceRefreshWidgets(context); @@ -189,7 +198,7 @@ public class RunCommandPlugin extends Plugin { return context.getString(R.string.pref_plugin_runcommand); } - public boolean canAddCommand(){ + public boolean canAddCommand() { return canAddCommand; } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.kt b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.kt index 3f50929d..ee6767c2 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.kt +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.kt @@ -20,19 +20,22 @@ import org.kde.kdeconnect_tp.R internal class RunCommandWidgetDataProvider(private val context: Context, val intent: Intent?) : RemoteViewsFactory { - private lateinit var deviceId : String + private var deviceId : String? = null private var widgetId : Int = AppWidgetManager.INVALID_APPWIDGET_ID override fun onCreate() { widgetId = intent?.getIntExtra(EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) ?: AppWidgetManager.INVALID_APPWIDGET_ID if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { - Log.e("KDEConnect/Widget", "RunCommandWidgetDataProvider: No widget id extra set") + Log.e("KDEConnect/Widget", "RunCommandWidgetDataProvider: No widget id extra was set") return } - deviceId = loadWidgetDeviceIdPref(context, widgetId)!! + deviceId = loadWidgetDeviceIdPref(context, widgetId) + } + + override fun onDataSetChanged() { + deviceId = loadWidgetDeviceIdPref(context, widgetId) } - override fun onDataSetChanged() {} override fun onDestroy() {} private fun getPlugin(): RunCommandPlugin? { @@ -48,7 +51,11 @@ internal class RunCommandWidgetDataProvider(private val context: Context, val in val plugin : RunCommandPlugin? = getPlugin() if (plugin == null) { - Log.e("getViewAt", "RunCommandWidgetDataProvider: Plugin not found"); + // Either the deviceId was null, or the plugin is not available. + if (deviceId != null) { + Log.e("getViewAt", "RunCommandWidgetDataProvider: Plugin not found") + } + // Return a new, not-configured layout as a fallback return remoteView } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetProvider.kt b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetProvider.kt index 64c0842c..6412867c 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetProvider.kt +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetProvider.kt @@ -15,7 +15,6 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log -import android.view.View import android.widget.RemoteViews import org.kde.kdeconnect.Device import org.kde.kdeconnect.KdeConnect @@ -87,6 +86,20 @@ fun forceRefreshWidgets(context : Context) { context.sendBroadcast(intent) } +/** + * Recreate the [RemoteViews] layout of a given widget. + * + * This function is called when a new widget is created, or when the list of devices changes, or if + * a device enables/disables its [RunCommandPlugin]. Hosting apps that contain our widgets will do + * anything they can to avoid extra renders. + * + * 1. We use [appWidgetId] as a request code in [assignTitleIntent] to force hosting apps to track a + * separate intent for each widget. + * 2. We call [AppWidgetManager.notifyAppWidgetViewDataChanged] at the end of this function, which + * lets the list adapter know that it might be referring to the wrong device id. + * + * See also [RunCommandWidgetDataProvider.onDataSetChanged]. + */ internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, @@ -94,46 +107,86 @@ internal fun updateAppWidget( ) { Log.d("WidgetProvider", "updateAppWidget: $appWidgetId") + // Determine which device provided these commands val deviceId = loadWidgetDeviceIdPref(context, appWidgetId) val device: Device? = if (deviceId != null) KdeConnect.getInstance().getDevice(deviceId) else null val views = RemoteViews(BuildConfig.APPLICATION_ID, R.layout.widget_remotecommandplugin) + assignTitleIntent(context, appWidgetId, views) + + Log.d("WidgetProvider", "updateAppWidget device: " + if (device == null) "null" else device.name) + + // Android should automatically toggle between the command list and the error text + views.setEmptyView(R.id.widget_command_list, R.id.widget_error_text) + + // TODO: Use string resources + + if (device == null) { + // There are two reasons we reach this condition: + // 1. there is no preference string for this widget id + // 2. the string id does not match any devices in KdeConnect.getInstance() + // In both cases, we want the user to assign a device to this widget + views.setTextViewText(R.id.widget_title_text, context.getString(R.string.kde_connect)) + views.setTextViewText(R.id.widget_error_text, "Whose commands should we show? Click the title to set a device.") + } else { + views.setTextViewText(R.id.widget_title_text, device.name) + val plugin = device.getPlugin(RunCommandPlugin::class.java) + if (device.isReachable) { + val message: String = if (plugin == null) { + "Device doesn't allow us to run commands." + } else { + "Device has no commands available." + } + views.setTextViewText(R.id.widget_error_text, message) + assignListAdapter(context, appWidgetId, views) + assignListIntent(context, appWidgetId, views) + } else { + views.setTextViewText(R.id.widget_error_text, context.getString(R.string.runcommand_notreachable)) + } + } + + appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.widget_command_list) + appWidgetManager.updateAppWidget(appWidgetId, views) +} + +/** + * Create an Intent to launch the config activity whenever the title is clicked. + * + * See [RunCommandWidgetConfigActivity]. + */ +private fun assignTitleIntent(context: Context, appWidgetId: Int, views: RemoteViews) { val setDeviceIntent = Intent(context, RunCommandWidgetConfigActivity::class.java) setDeviceIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId) // We pass appWidgetId as requestCode even if it's not used to force the creation a new PendingIntent // instead of reusing an existing one, which is what happens if only the "extras" field differs. // Docs: https://developer.android.com/reference/android/app/PendingIntent.html val setDevicePendingIntent = PendingIntent.getActivity(context, appWidgetId, setDeviceIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE) - views.setOnClickPendingIntent(R.id.runcommandWidgetTitleHeader, setDevicePendingIntent) - - Log.d("WidgetProvider", "updateAppWidget device: " + if (device == null) "null" else device.name) - - if (device == null) { - views.setTextViewText(R.id.runcommandWidgetTitle, context.getString(R.string.kde_connect)) - views.setViewVisibility(R.id.run_commands_list, View.VISIBLE) - views.setViewVisibility(R.id.not_reachable_message, View.GONE) - } else { - views.setTextViewText(R.id.runcommandWidgetTitle, device.name) - if (device.isReachable) { - views.setViewVisibility(R.id.run_commands_list, View.VISIBLE) - views.setViewVisibility(R.id.not_reachable_message, View.GONE) - // Configure remote adapter - val dataProviderIntent = Intent(context, CommandsRemoteViewsService::class.java) - dataProviderIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - dataProviderIntent.data = Uri.parse(dataProviderIntent.toUri(Intent.URI_INTENT_SCHEME)) - views.setRemoteAdapter(R.id.run_commands_list, dataProviderIntent) - // This pending intent allows the remote adapter to call fillInIntent so list items can do things - val runCommandTemplateIntent = Intent(context, RunCommandWidgetProvider::class.java) - runCommandTemplateIntent.action = RUN_COMMAND_ACTION - runCommandTemplateIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - val runCommandTemplatePendingIntent = PendingIntent.getBroadcast(context, appWidgetId, runCommandTemplateIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) - views.setPendingIntentTemplate(R.id.run_commands_list, runCommandTemplatePendingIntent) - } else { - views.setViewVisibility(R.id.run_commands_list, View.GONE) - views.setViewVisibility(R.id.not_reachable_message, View.VISIBLE) - } - } - - appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.run_commands_list) - appWidgetManager.updateAppWidget(appWidgetId, views) + views.setOnClickPendingIntent(R.id.widget_title_wrapper, setDevicePendingIntent) +} + +/** + * Configure remote adapter + * + * This function can only be called once in the lifetime of the widget. Subsequent calls do nothing. + * Use [RunCommandWidgetConfigActivity] and the config function [saveWidgetDeviceIdPref] to change + * the adapter's behavior. + */ +private fun assignListAdapter(context: Context, appWidgetId: Int, views: RemoteViews) { + val dataProviderIntent = Intent(context, CommandsRemoteViewsService::class.java) + dataProviderIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + dataProviderIntent.data = Uri.parse(dataProviderIntent.toUri(Intent.URI_INTENT_SCHEME)) + views.setRemoteAdapter(R.id.widget_command_list, dataProviderIntent) +} + +/** + * This pending intent allows the remote adapter to call fillInIntent so list items can do things. + * + * See [RemoteViews.setOnClickFillInIntent]. + */ +private fun assignListIntent(context: Context, appWidgetId: Int, views: RemoteViews) { + val runCommandTemplateIntent = Intent(context, RunCommandWidgetProvider::class.java) + runCommandTemplateIntent.action = RUN_COMMAND_ACTION + runCommandTemplateIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + val runCommandTemplatePendingIntent = PendingIntent.getBroadcast(context, appWidgetId, runCommandTemplateIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) + views.setPendingIntentTemplate(R.id.widget_command_list, runCommandTemplatePendingIntent) }