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

Add ability to run commands from Android 11 power menu

This commit is contained in:
Maxim Leshchenko 2021-06-24 23:53:00 +03:00 committed by Albert Vaca Cintora
parent 870c40e412
commit 7286b8a24a
5 changed files with 225 additions and 0 deletions

View File

@ -396,6 +396,13 @@
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<service android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandControlsProviderService" android:label="@string/kde_connect"
android:permission="android.permission.BIND_CONTROLS">
<intent-filter>
<action android:name="android.service.controls.ControlsProviderService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -198,6 +198,10 @@ dependencies {
testImplementation 'org.powermock:powermock-api-mockito2:2.0.0'
testImplementation 'org.mockito:mockito-core:2.23.0'
testImplementation 'org.skyscreamer:jsonassert:1.3.0'
// For device controls
implementation 'org.reactivestreams:reactive-streams:1.0.3'
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
}
repositories {

View File

@ -534,5 +534,6 @@
<string name="everyone_else">Everyone else who has contributed to KDE Connect over the years</string>
<string name="send_clipboard">Send clipboard</string>
<string name="tap_to_execute">Tap to execute</string>
</resources>

View File

@ -0,0 +1,194 @@
/*
* SPDX-FileCopyrightText: 2021 Maxim Leshchenko <cnmaks90@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins.RunCommandPlugin
import android.app.PendingIntent
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.drawable.Icon
import android.service.controls.Control
import android.service.controls.ControlsProviderService
import android.service.controls.actions.CommandAction
import android.service.controls.actions.ControlAction
import android.service.controls.templates.StatelessTemplate
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.preference.PreferenceManager
import io.reactivex.Flowable
import io.reactivex.processors.ReplayProcessor
import org.json.JSONArray
import org.json.JSONException
import org.kde.kdeconnect.BackgroundService
import org.kde.kdeconnect.Device
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)
@RequiresApi(30)
class RunCommandControlsProviderService : ControlsProviderService() {
private lateinit var updatePublisher: ReplayProcessor<Control>
private lateinit var sharedPreferences: SharedPreferences
override fun createPublisherForAllAvailable(): Flow.Publisher<Control> {
return FlowAdapters.toFlowPublisher(Flowable.fromIterable(getAllCommandsList().map { commandEntry ->
Control.StatelessBuilder(commandEntry.device.deviceId + "-" + commandEntry.key, getIntent(commandEntry.device))
.setTitle(commandEntry.name)
.setSubtitle(commandEntry.command)
.setStructure(commandEntry.device.name)
.setCustomIcon(Icon.createWithResource(this, R.drawable.run_command_plugin_icon_24dp))
.build()
}))
}
override fun createPublisherFor(controlIds: MutableList<String>): Flow.Publisher<Control> {
updatePublisher = ReplayProcessor.create()
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)
.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)
.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))
.setStatus(Control.STATUS_NOT_FOUND)
.build())
}
}
}
return FlowAdapters.toFlowPublisher(updatePublisher)
}
override fun performControlAction(controlId: String, action: ControlAction, consumer: Consumer<Int>) {
if (!this::updatePublisher.isInitialized) {
updatePublisher = ReplayProcessor.create()
}
if (action is CommandAction) {
val commandEntry = getCommandByControlId(controlId)
if (commandEntry != null) {
val plugin = BackgroundService.getInstance().getDevice(controlId.split("-")[0]).getPlugin(RunCommandPlugin::class.java)
if (plugin != null) {
plugin.runCommand(commandEntry.key)
consumer.accept(ControlAction.RESPONSE_OK)
} else {
consumer.accept(ControlAction.RESPONSE_FAIL)
}
updatePublisher.onNext(Control.StatefulBuilder(controlId, getIntent(commandEntry.device))
.setTitle(commandEntry.name)
.setSubtitle(commandEntry.command)
.setStructure(commandEntry.device.name)
.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())
}
}
}
private fun getSavedCommandsList(device: Device): List<CommandEntryWithDevice> {
if (!this::sharedPreferences.isInitialized) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
}
val commandList = mutableListOf<CommandEntryWithDevice>()
return try {
val jsonArray = JSONArray(sharedPreferences.getString(RunCommandPlugin.KEY_COMMANDS_PREFERENCE + device.deviceId, "[]"))
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
} catch (error: JSONException) {
Log.e("RunCommand", "Error parsing JSON", error)
listOf()
}
}
private fun getAllCommandsList(): List<CommandEntryWithDevice> {
val commandList = mutableListOf<CommandEntryWithDevice>()
for (device in BackgroundService.getInstance().devices.values) {
if (!device.isReachable) {
commandList.addAll(getSavedCommandsList(device))
continue
} else if (!device.isPaired) {
continue
}
val plugin = device.getPlugin(RunCommandPlugin::class.java)
if (plugin != null) {
for (jsonObject in plugin.commandList) {
try {
commandList.add(CommandEntryWithDevice(jsonObject.getString("name"), jsonObject.getString("command"), jsonObject.getString("key"), device))
} catch (error: JSONException) {
Log.e("RunCommand", "Error parsing JSON", error)
}
}
}
}
return Collections.unmodifiableList(commandList)
}
private fun getCommandByControlId(controlId: String): CommandEntryWithDevice? {
val controlIdParts = controlId.split("-")
val device = BackgroundService.getInstance().getDevice(controlIdParts[0])
if (!device.isPaired) return null
val commandList = if (device.isReachable) {
device?.getPlugin(RunCommandPlugin::class.java)?.commandList?.map { jsonObject ->
CommandEntryWithDevice(jsonObject.getString("name"), jsonObject.getString("command"), jsonObject.getString("key"), device)
}
} else {
getSavedCommandsList(device)
}
return commandList?.find { command ->
try {
command.key == controlIdParts[1]
} catch (error: JSONException) {
Log.e("RunCommand", "Error parsing JSON", error)
false
}
}
}
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)
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
}
}

View File

@ -9,9 +9,12 @@ package org.kde.kdeconnect.Plugins.RunCommandPlugin;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.kde.kdeconnect.NetworkPacket;
@ -25,17 +28,21 @@ import java.util.Comparator;
import java.util.Iterator;
import androidx.core.content.ContextCompat;
import androidx.preference.PreferenceManager;
@PluginFactory.LoadablePlugin
public class RunCommandPlugin extends Plugin {
private final static String PACKET_TYPE_RUNCOMMAND = "kdeconnect.runcommand";
private final static String PACKET_TYPE_RUNCOMMAND_REQUEST = "kdeconnect.runcommand.request";
public final static String KEY_COMMANDS_PREFERENCE = "commands_preference_";
private final ArrayList<JSONObject> commandList = new ArrayList<>();
private final ArrayList<CommandsChangedCallback> callbacks = new ArrayList<>();
private final ArrayList<CommandEntry> commandItems = new ArrayList<>();
private SharedPreferences sharedPreferences;
private boolean canAddCommand;
public void addCommandsUpdatedCallback(CommandsChangedCallback newCallback) {
@ -75,6 +82,7 @@ public class RunCommandPlugin extends Plugin {
@Override
public boolean onCreate() {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context);
requestCommandList();
return true;
}
@ -109,6 +117,17 @@ public class RunCommandPlugin extends Plugin {
Collections.sort(commandItems, Comparator.comparing(CommandEntry::getName));
// Used only by RunCommandControlsProviderService to display controls correctly even when device is not available
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
JSONArray array = new JSONArray();
for (JSONObject command : commandList) {
array.put(command);
}
sharedPreferences.edit().putString(KEY_COMMANDS_PREFERENCE + device.getDeviceId(), array.toString()).apply();
}
Intent updateWidget = new Intent(context, RunCommandWidget.class);
context.sendBroadcast(updateWidget);