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:
parent
184eab4552
commit
548b636f32
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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>
|
||||
|
32
res/xml/runcommand_preferences.xml
Normal file
32
res/xml/runcommand_preferences.xml
Normal 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>
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user