2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 01:51:47 +00:00

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
This commit is contained in:
Philip Cohn-Cort 2023-10-23 21:58:08 +00:00
parent 184eab4552
commit 548b636f32
12 changed files with 227 additions and 101 deletions

View File

@ -53,7 +53,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<application
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="KDE Connect"
android:label="@string/kde_connect"
android:supportsRtl="true"
android:allowBackup="false"
android:dataExtractionRules="@xml/data_extraction_rules"
@ -99,7 +99,6 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<activity
android:name="org.kde.kdeconnect.UserInterface.MainActivity"
android:label="KDE Connect"
android:exported="true"
android:theme="@style/KdeConnectTheme.NoActionBar">
<intent-filter>

View File

@ -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" />
<TextView
android:id="@+id/add_comand_explanation"
android:id="@+id/add_command_explanation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"

View File

@ -8,14 +8,14 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/kde_connect"/>
tools:title="@string/kde_connect"/>
</com.google.android.material.appbar.AppBarLayout>

View File

@ -16,13 +16,14 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
tools:ignore="RtlSymmetry">
<LinearLayout
android:id="@+id/runcommandWidgetTitleHeader"
android:id="@+id/widget_title_wrapper"
android:background="@color/on_secondary"
android:gravity="center_vertical|start"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/widget_title_icon"
android:padding="8dip"
android:paddingEnd="6dip"
android:layout_width="wrap_content"
@ -33,7 +34,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
tools:ignore="UseAppTint"/> <!-- can't use app:tint in RemoteView -->
<TextView
android:id="@+id/runcommandWidgetTitle"
android:id="@+id/widget_title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
@ -45,7 +46,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
</LinearLayout>
<ListView
android:id="@+id/run_commands_list"
android:id="@+id/widget_command_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:addStatesFromChildren="true"
@ -53,7 +54,7 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
android:orientation="vertical" />
<TextView
android:id="@+id/not_reachable_message"
android:id="@+id/widget_error_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawablePadding="8dip"

View File

@ -368,10 +368,17 @@ SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted
<string name="mpris_stop">Stop the current player</string>
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
<string name="clipboard_toast">Copied to clipboard</string>
<string name="runcommand_notreachable">Device is not reachable</string>
<string name="runcommand_notpaired">Device is not paired</string>
<string name="runcommand_nosuchdevice">There is no such device</string>
<string name="runcommand_noruncommandplugin">This device does not have the Run Command Plugin enabled</string>
<string name="runcommand_category_device_controls_title">Device Controls</string>
<string name="runcommand_category_device_controls" translatable="false">runcommand_category_device_controls</string>
<string name="runcommand_device_controls_summary">If your device supports Device Controls, commands you have configured will appear there.</string>
<string name="set_runcommand_name_as_title">set_runcommand_name_as_title</string>
<string name="runcommand_name_as_title_title">Show name as title</string>
<string name="pref_plugin_findremotedevice">Find remote device</string>
<string name="pref_plugin_findremotedevice_desc">Ring your remote device</string>
<string name="ring">Ring</string>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2023 Philip Cohn-Cort <cliabhach@gmail.com>
SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
-->
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:keep="@xml/runcommand_preferences">
<org.kde.kdeconnect.Helpers.LongSummaryPreferenceCategory
android:title="@string/runcommand_category_device_controls_title"
android:key="@string/runcommand_category_device_controls"
android:summary="@string/runcommand_device_controls_summary"
>
<SwitchPreference
android:id="@+id/runcommand_name_as_title_preference"
android:defaultValue="true"
android:key="@string/set_runcommand_name_as_title"
android:summary="Name -> title"
android:summaryOff="Name -> subtitle"
android:title="@string/runcommand_name_as_title_title"
/>
</org.kde.kdeconnect.Helpers.LongSummaryPreferenceCategory>
</PreferenceScreen>

View File

@ -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;

View File

@ -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();
}

View File

@ -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)
}
}

View File

@ -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<String> 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;
}

View File

@ -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
}

View File

@ -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)
}