Members of the KDE Community are recommended to subscribe to the kde-community mailing list at https://mail.kde.org/mailman/listinfo/kde-community to allow them to participate in important discussions and receive other important announcements

Commit e0fde165 authored by Nicolas Fella's avatar Nicolas Fella

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
parent 5c4c402f
......@@ -31,6 +31,7 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.BIND_REMOTEVIEWS" />
<application
android:allowBackup="true"
......@@ -161,6 +162,28 @@
</activity>
<activity android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetDeviceSelector"
android:theme="@style/Theme.AppCompat.Light.Dialog"
android:launchMode="singleTask"
android:screenOrientation="user"
android:noHistory="true"
android:excludeFromRecents="true"
android:label="@string/pref_plugin_runcommand"/>
<service
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetDataProviderService"
android:exported="false" android:permission="android.permission.BIND_REMOTEVIEWS"/>
<receiver android:label="@string/pref_plugin_runcommand" android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidget">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="RUN_COMMAND_ACTION" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/remotecommandplugin_widget" />
</receiver>
<activity android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandUrlActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
......
......@@ -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">
<!-- We should use android:background="@android:/listChoiceBackgroundIndicator"
instead of abc_list_selector_holo_dark but that's only supported from API11 -->
......
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:background="#FFFFFF"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/runcommandWidgetTitleHeader"
android:background="@color/primary"
android:gravity="center_vertical|left"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:paddingTop="6dip"
android:paddingBottom="6dip"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon"/>
<TextView
android:id="@+id/runcommandWidgetTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceLarge" />
</LinearLayout>
<ListView
android:id="@+id/runcommandslist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="4dp"
android:addStatesFromChildren="true"
android:visibility="gone"
android:orientation="vertical" />
<TextView
android:id="@+id/not_reachable_message"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:drawableLeft="@drawable/ic_error_outline_black_48dp"
android:drawablePadding="8dip"
android:paddingLeft="8dip"
android:drawableStart="@drawable/ic_error_outline_black_48dp"
android:gravity="center_vertical"
android:text="@string/unreachable_description"
android:textAppearance="?android:attr/textAppearanceMedium" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="20dp">
<ListView
android:id="@+id/runcommandsdevicelist"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="4dp"
android:addStatesFromChildren="true"
android:orientation="vertical" />
</LinearLayout>
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="180dp" android:minHeight="80dp"
android:initialLayout="@layout/widget_remotecommandplugin" android:resizeMode="horizontal|vertical" android:previewImage="@drawable/icon" />
......@@ -37,4 +37,8 @@ public class CommandEntry extends EntryItem {
public String getName() {
return title;
}
public String getCommand() {
return subtitle;
}
}
......@@ -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<JSONObject> commandList = new ArrayList<>();
private ArrayList<CommandsChangedCallback> callbacks = new ArrayList<>();
private final ArrayList<CommandEntry> commandItems = new ArrayList<>();
private boolean canAddCommand;
......@@ -61,6 +63,10 @@ public class RunCommandPlugin extends Plugin {
return commandList;
}
public ArrayList<CommandEntry> 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<String> 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();
}
......
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
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
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
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<ListAdapter.Item> 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment