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:
parent
870c40e412
commit
7286b8a24a
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user