From e0fde1652522e170fc48a913a9244fdf2cede88f Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Mon, 30 Jul 2018 18:15:00 +0200 Subject: [PATCH] Added runcommandplugin widget Summary: A simple widget to execute commands from android desktop Test Plan: Add the widget and execute commands from it. By default, the selected device is the first device in the device list. The selected device can be changed via the widget title. If there is no commands, the widget show an empty list If there is no connected device, show the message located in @string/unreachable_description Reviewers: albertvaka, nicolasfella, #kde_connect Reviewed By: albertvaka Subscribers: kdeconnect, nicolasfella Tags: #kde_connect Differential Revision: https://phabricator.kde.org/D12507 --- AndroidManifest.xml | 23 +++ res/layout/list_item_entry.xml | 3 +- res/layout/widget_remotecommandplugin.xml | 50 ++++++ .../widget_remotecommandplugin_dialog.xml | 16 ++ res/xml/remotecommandplugin_widget.xml | 3 + .../RunCommandPlugin/CommandEntry.java | 4 + .../RunCommandPlugin/RunCommandPlugin.java | 25 +++ .../RunCommandPlugin/RunCommandWidget.java | 154 ++++++++++++++++++ .../RunCommandWidgetDataProvider.java | 93 +++++++++++ .../RunCommandWidgetDataProviderService.java | 16 ++ .../RunCommandWidgetDeviceSelector.java | 61 +++++++ 11 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 res/layout/widget_remotecommandplugin.xml create mode 100644 res/layout/widget_remotecommandplugin_dialog.xml create mode 100644 res/xml/remotecommandplugin_widget.xml create mode 100644 src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidget.java create mode 100644 src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.java create mode 100644 src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProviderService.java create mode 100644 src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDeviceSelector.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 98377083..c7ba3fce 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -31,6 +31,7 @@ + + + + + + + + + + + + + + + diff --git a/res/layout/list_item_entry.xml b/res/layout/list_item_entry.xml index fcbbae3a..9a0f7199 100644 --- a/res/layout/list_item_entry.xml +++ b/res/layout/list_item_entry.xml @@ -11,7 +11,8 @@ android:paddingEnd="?android:attr/scrollbarSize" android:paddingLeft="12dip" android:paddingRight="?android:attr/scrollbarSize" - android:paddingStart="12dip"> + android:paddingStart="12dip" + android:id="@+id/list_item_entry"> diff --git a/res/layout/widget_remotecommandplugin.xml b/res/layout/widget_remotecommandplugin.xml new file mode 100644 index 00000000..d0d77132 --- /dev/null +++ b/res/layout/widget_remotecommandplugin.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/res/layout/widget_remotecommandplugin_dialog.xml b/res/layout/widget_remotecommandplugin_dialog.xml new file mode 100644 index 00000000..501ab2c1 --- /dev/null +++ b/res/layout/widget_remotecommandplugin_dialog.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/res/xml/remotecommandplugin_widget.xml b/res/xml/remotecommandplugin_widget.xml new file mode 100644 index 00000000..ea34c8a2 --- /dev/null +++ b/res/xml/remotecommandplugin_widget.xml @@ -0,0 +1,3 @@ + + diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java index d9f97648..5eeac327 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/CommandEntry.java @@ -37,4 +37,8 @@ public class CommandEntry extends EntryItem { public String getName() { return title; } + + public String getCommand() { + return subtitle; + } } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java index 23026898..8451ac30 100644 --- a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandPlugin.java @@ -33,6 +33,7 @@ import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; +import java.util.Collections; import java.util.Iterator; public class RunCommandPlugin extends Plugin { @@ -42,6 +43,7 @@ public class RunCommandPlugin extends Plugin { private ArrayList commandList = new ArrayList<>(); private ArrayList callbacks = new ArrayList<>(); + private final ArrayList commandItems = new ArrayList<>(); private boolean canAddCommand; @@ -61,6 +63,10 @@ public class RunCommandPlugin extends Plugin { return commandList; } + public ArrayList getCommandItems() { + return commandItems; + } + @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_runcommand); @@ -88,6 +94,7 @@ public class RunCommandPlugin extends Plugin { if (np.has("commandList")) { commandList.clear(); try { + commandItems.clear(); JSONObject obj = new JSONObject(np.getString("commandList")); Iterator keys = obj.keys(); while (keys.hasNext()) { @@ -95,7 +102,25 @@ public class RunCommandPlugin extends Plugin { 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") + ) + ); + } catch (JSONException e) { + e.printStackTrace(); + } } + + Collections.sort(commandItems, (lhs, rhs) -> lhs.getName().compareTo(rhs.getName()) ); + + Intent updateWidget = new Intent(context, RunCommandWidget.class); + context.sendBroadcast(updateWidget); + } catch (JSONException e) { e.printStackTrace(); } diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidget.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidget.java new file mode 100644 index 00000000..029e0068 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidget.java @@ -0,0 +1,154 @@ +package org.kde.kdeconnect.Plugins.RunCommandPlugin; + +import android.app.PendingIntent; +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.Log; +import android.view.View; +import android.widget.RemoteViews; + +import org.kde.kdeconnect.BackgroundService; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect_tp.R; + +@RequiresApi( api = Build.VERSION_CODES.ICE_CREAM_SANDWICH ) +public class RunCommandWidget extends AppWidgetProvider { + + public static final String RUN_COMMAND_ACTION = "RUN_COMMAND_ACTION"; + public static final String TARGET_COMMAND = "TARGET_COMMAND"; + public static final String TARGET_DEVICE = "TARGET_DEVICE"; + public static final String SET_CURRENT_DEVICE = "SET_CURRENT_DEVICE"; + + private static String currentDeviceId; + + @Override + public void onReceive(Context context, Intent intent) { + + super.onReceive(context, intent); + + if (intent != null && intent.getAction() != null && intent.getAction().equals(RUN_COMMAND_ACTION)) { + + final String targetCommand = intent.getStringExtra(TARGET_COMMAND); + final String targetDevice = intent.getStringExtra(TARGET_DEVICE); + + BackgroundService.RunCommand(context, service -> { + RunCommandPlugin plugin = service.getDevice(targetDevice).getPlugin(RunCommandPlugin.class); + + if (plugin != null) { + try { + + plugin.runCommand(targetCommand); + } catch (Exception ex) { + Log.e("RunCommandWidget", "Error running command", ex); + } + } + }); + } else if (intent != null && intent.getAction() != null && intent.getAction().equals(SET_CURRENT_DEVICE)) { + setCurrentDevice(context); + } + + final Intent newIntent = new Intent(context, RunCommandWidgetDataProviderService.class); + context.startService(newIntent); + updateWidget(context); + + } + + private void setCurrentDevice(final Context context) { + Intent popup = new Intent(context, RunCommandWidgetDeviceSelector.class); + popup.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + + context.startActivity(popup); + } + + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + updateWidget(context); + } + + private void updateWidget(final Context context) { + + if (getCurrentDevice() == null || !getCurrentDevice().isReachable()) { + + BackgroundService.RunCommand(context, service -> { + if (service.getDevices().size() > 0) + currentDeviceId = service.getDevices().elements().nextElement().getDeviceId(); + + updateWidgetImpl(context); + }); + } + + updateWidgetImpl(context); + } + + private void updateWidgetImpl(Context context) { + + try { + + AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context); + int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context, RunCommandWidget.class)); + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_remotecommandplugin); + + PendingIntent pendingIntent; + Intent intent; + + intent = new Intent(context, RunCommandWidget.class); + intent.setAction(SET_CURRENT_DEVICE); + pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setOnClickPendingIntent(R.id.runcommandWidgetTitleHeader, pendingIntent); + + if (getCurrentDevice() == null || !getCurrentDevice().isReachable()) { + views.setTextViewText(R.id.runcommandWidgetTitle, context.getString(R.string.pref_plugin_runcommand)); + views.setViewVisibility(R.id.runcommandslist, View.GONE); + views.setViewVisibility(R.id.not_reachable_message, View.VISIBLE); + } else { + views.setTextViewText(R.id.runcommandWidgetTitle, getCurrentDevice().getName()); + views.setViewVisibility(R.id.runcommandslist, View.VISIBLE); + views.setViewVisibility(R.id.not_reachable_message, View.GONE); + } + + for (int appWidgetId : appWidgetIds) { + + intent = new Intent(context, RunCommandWidgetDataProviderService.class); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId); + intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); + views.setRemoteAdapter(R.id.runcommandslist, intent); + + intent = new Intent(context, RunCommandWidget.class); + pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); + views.setPendingIntentTemplate(R.id.runcommandslist, pendingIntent); + + AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, views); + AppWidgetManager.getInstance(context).notifyAppWidgetViewDataChanged(appWidgetId, R.id.runcommandslist); + + } + + } catch (Exception ex) { + Log.e("RunCommandWidget", "Error updating widget", ex); + } + + if (BackgroundService.getInstance() != null) { + BackgroundService.getInstance().addDeviceListChangedCallback("RunCommandWidget", () -> { + Intent updateWidget = new Intent(context, RunCommandWidget.class); + context.sendBroadcast(updateWidget); + }); + } + } + + public static Device getCurrentDevice() { + + try { + return BackgroundService.getInstance().getDevice(currentDeviceId); + } catch (Exception ex) { + return null; + } + } + + public static void setCurrentDevice(final String DeviceId) { + currentDeviceId = DeviceId; + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.java new file mode 100644 index 00000000..e6441a9f --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProvider.java @@ -0,0 +1,93 @@ +package org.kde.kdeconnect.Plugins.RunCommandPlugin; + +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.view.View; +import android.widget.RemoteViews; +import android.widget.RemoteViewsService; + +import org.kde.kdeconnect_tp.R; + + +@RequiresApi( api = Build.VERSION_CODES.ICE_CREAM_SANDWICH ) +public class RunCommandWidgetDataProvider implements RemoteViewsService.RemoteViewsFactory { + + private final Context mContext; + + public RunCommandWidgetDataProvider(Context context, Intent intent) { + mContext = context; + } + + private boolean checkPlugin() { + if (RunCommandWidget.getCurrentDevice() == null || !RunCommandWidget.getCurrentDevice().isReachable()) + return false; + + return RunCommandWidget.getCurrentDevice().getPlugin(RunCommandPlugin.class) != null; + } + + @Override + public void onCreate() { + } + + @Override + public void onDataSetChanged() { + + } + + @Override + public void onDestroy() { + } + + @Override + public int getCount() { + return checkPlugin() ? RunCommandWidget.getCurrentDevice().getPlugin(RunCommandPlugin.class).getCommandItems().size() : 0; + } + + @Override + public RemoteViews getViewAt(int i) { + + RemoteViews remoteView = new RemoteViews(mContext.getPackageName(), R.layout.list_item_entry); + + if (checkPlugin() && RunCommandWidget.getCurrentDevice().getPlugin(RunCommandPlugin.class).getCommandItems().size() > i) { + CommandEntry listItem = RunCommandWidget.getCurrentDevice().getPlugin(RunCommandPlugin.class).getCommandItems().get(i); + + final Intent configIntent = new Intent(mContext, RunCommandWidget.class); + configIntent.setAction(RunCommandWidget.RUN_COMMAND_ACTION); + configIntent.putExtra(RunCommandWidget.TARGET_COMMAND, listItem.getKey()); + configIntent.putExtra(RunCommandWidget.TARGET_DEVICE, RunCommandWidget.getCurrentDevice().getDeviceId()); + + remoteView.setTextViewText(R.id.list_item_entry_title, listItem.getName()); + remoteView.setTextViewText(R.id.list_item_entry_summary, listItem.getCommand()); + remoteView.setViewVisibility(R.id.list_item_entry_summary, View.VISIBLE); + + remoteView.setOnClickFillInIntent(R.id.list_item_entry, configIntent); + } + + return remoteView; + } + + @Override + public RemoteViews getLoadingView() { + return null; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public long getItemId(int i) { + if (RunCommandWidget.getCurrentDevice() != null) + return RunCommandWidget.getCurrentDevice().getPlugin(RunCommandPlugin.class).getCommandItems().get(i).getKey().hashCode(); + + return 0; + } + + @Override + public boolean hasStableIds() { + return false; + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProviderService.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProviderService.java new file mode 100644 index 00000000..12106da0 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDataProviderService.java @@ -0,0 +1,16 @@ +package org.kde.kdeconnect.Plugins.RunCommandPlugin; + +import android.content.Intent; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.widget.RemoteViewsService; + +@RequiresApi( api = Build.VERSION_CODES.ICE_CREAM_SANDWICH ) +public class RunCommandWidgetDataProviderService extends RemoteViewsService { + + @Override + public RemoteViewsFactory onGetViewFactory(Intent intent) { + + return (new RunCommandWidgetDataProvider(RunCommandWidgetDataProviderService.this, intent)); + } +} \ No newline at end of file diff --git a/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDeviceSelector.java b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDeviceSelector.java new file mode 100644 index 00000000..c9167e52 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RunCommandPlugin/RunCommandWidgetDeviceSelector.java @@ -0,0 +1,61 @@ +package org.kde.kdeconnect.Plugins.RunCommandPlugin; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.RequiresApi; +import android.support.v7.app.AppCompatActivity; +import android.view.Window; +import android.widget.ListView; + +import org.kde.kdeconnect.BackgroundService; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.UserInterface.List.ListAdapter; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.Collections; + +@RequiresApi( api = Build.VERSION_CODES.ICE_CREAM_SANDWICH ) +public class RunCommandWidgetDeviceSelector extends AppCompatActivity { + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + + requestWindowFeature(Window.FEATURE_NO_TITLE); + setContentView(R.layout.widget_remotecommandplugin_dialog); + + BackgroundService.RunCommand(this, service -> runOnUiThread(() -> { + ListView view = (ListView) findViewById(R.id.runcommandsdevicelist); + + final ArrayList deviceItems = new ArrayList<>(); + + for (Device device : service.getDevices().values()) { + if (device.isPaired() && device.isReachable()) { + deviceItems.add( + new CommandEntry( + device.getName(), + null, + device.getDeviceId() + ) + ); + } + } + + Collections.sort(deviceItems, (lhs, rhs) -> ((CommandEntry) lhs).getName().compareTo(((CommandEntry) rhs).getName())); + + ListAdapter adapter = new ListAdapter(RunCommandWidgetDeviceSelector.this, deviceItems); + + view.setAdapter(adapter); + view.setOnItemClickListener((adapterView, viewContent, i, l) -> { + CommandEntry entry = (CommandEntry) deviceItems.get(i); + RunCommandWidget.setCurrentDevice(entry.getKey()); + + Intent updateWidget = new Intent(RunCommandWidgetDeviceSelector.this, RunCommandWidget.class); + RunCommandWidgetDeviceSelector.this.sendBroadcast(updateWidget); + + finish(); + }); + })); + } +} \ No newline at end of file