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

Migrate PluginFactory to Kotlin

This commit is contained in:
TPJ Schikhof 2024-09-28 04:59:56 +00:00 committed by Philip Cohn-Cort
parent 8fb545d620
commit 854b2a1c9f
6 changed files with 101 additions and 187 deletions

View File

@ -112,7 +112,7 @@ class Device : PacketReceiver {
this.settings = context.getSharedPreferences(deviceId, Context.MODE_PRIVATE)
this.deviceInfo = loadFromSettings(context, deviceId, settings)
this.pairingHandler = PairingHandler(this, createDefaultPairingCallback(), PairingHandler.PairState.Paired)
this.supportedPlugins = Vector(PluginFactory.getAvailablePlugins()) // Assume all are supported until we receive capabilities
this.supportedPlugins = Vector(PluginFactory.availablePlugins) // Assume all are supported until we receive capabilities
Log.i("Device", "Loading trusted device: ${deviceInfo.name}")
}
@ -126,7 +126,7 @@ class Device : PacketReceiver {
this.deviceInfo = link.deviceInfo
this.settings = context.getSharedPreferences(deviceInfo.id, Context.MODE_PRIVATE)
this.pairingHandler = PairingHandler(this, createDefaultPairingCallback(), PairingHandler.PairState.NotPaired)
this.supportedPlugins = Vector(PluginFactory.getAvailablePlugins()) // Assume all are supported until we receive capabilities
this.supportedPlugins = Vector(PluginFactory.availablePlugins) // Assume all are supported until we receive capabilities
Log.i("Device", "Creating untrusted device: " + deviceInfo.name)
addLink(link)
}
@ -622,7 +622,7 @@ class Device : PacketReceiver {
supportedPlugins.forEach { pluginKey ->
val pluginInfo = PluginFactory.getPluginInfo(pluginKey)
val listenToUnpaired = pluginInfo.listenToUnpaired()
val listenToUnpaired = pluginInfo.listenToUnpaired
val pluginEnabled = (isPaired || listenToUnpaired) && this.isReachable && isPluginEnabled(pluginKey)

View File

@ -160,8 +160,8 @@ object DeviceHelper {
getDeviceName(context),
deviceType,
ProtocolVersion,
PluginFactory.getIncomingCapabilities(),
PluginFactory.getOutgoingCapabilities()
PluginFactory.incomingCapabilities,
PluginFactory.outgoingCapabilities
)
}

View File

@ -1,179 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins;
import static org.apache.commons.collections4.SetUtils.unmodifiableSet;
import android.content.Context;
import android.util.Log;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.atteo.classindex.ClassIndex;
import org.atteo.classindex.IndexAnnotated;
import org.kde.kdeconnect.Device;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class PluginFactory {
public static void sortPluginList(@NonNull List<String> plugins) {
plugins.sort(Comparator.comparing(o -> pluginInfo.get(o).displayName));
}
@IndexAnnotated
public @interface LoadablePlugin { } //Annotate plugins with this so PluginFactory finds them
public static class PluginInfo {
PluginInfo(@NonNull String displayName, @NonNull String description, @DrawableRes int icon,
boolean enabledByDefault, boolean hasSettings, boolean supportsDeviceSpecificSettings,
boolean listenToUnpaired, @NonNull String[] supportedPacketTypes, @NonNull String[] outgoingPacketTypes,
@NonNull Class<? extends Plugin> instantiableClass) {
this.displayName = displayName;
this.description = description;
this.icon = icon;
this.enabledByDefault = enabledByDefault;
this.hasSettings = hasSettings;
this.supportsDeviceSpecificSettings = supportsDeviceSpecificSettings;
this.listenToUnpaired = listenToUnpaired;
this.supportedPacketTypes = unmodifiableSet(supportedPacketTypes);
this.outgoingPacketTypes = unmodifiableSet(outgoingPacketTypes);
this.instantiableClass = instantiableClass;
}
public @NonNull String getDisplayName() {
return displayName;
}
public @NonNull String getDescription() {
return description;
}
public @DrawableRes int getIcon() {
return icon;
}
public boolean hasSettings() {
return hasSettings;
}
public boolean supportsDeviceSpecificSettings() { return supportsDeviceSpecificSettings; }
public boolean isEnabledByDefault() {
return enabledByDefault;
}
public boolean listenToUnpaired() {
return listenToUnpaired;
}
Set<String> getOutgoingPacketTypes() {
return outgoingPacketTypes;
}
public Set<String> getSupportedPacketTypes() {
return supportedPacketTypes;
}
Class<? extends Plugin> getInstantiableClass() {
return instantiableClass;
}
private final @NonNull String displayName;
private final @NonNull String description;
private final @DrawableRes int icon;
private final boolean enabledByDefault;
private final boolean hasSettings;
private final boolean supportsDeviceSpecificSettings;
private final boolean listenToUnpaired;
private final @NonNull Set<String> supportedPacketTypes;
private final @NonNull Set<String> outgoingPacketTypes;
private final Class<? extends Plugin> instantiableClass;
}
private static final Map<String, PluginInfo> pluginInfo = new ConcurrentHashMap<>();
public static PluginInfo getPluginInfo(String pluginKey) {
return pluginInfo.get(pluginKey);
}
public static void initPluginInfo(Context context) {
try {
for (Class<?> pluginClass : ClassIndex.getAnnotated(LoadablePlugin.class)) {
Plugin p = ((Plugin) pluginClass.newInstance());
p.setContext(context, null);
PluginInfo info = new PluginInfo(p.getDisplayName(), p.getDescription(), p.getIcon(),
p.isEnabledByDefault(), p.hasSettings(), p.supportsDeviceSpecificSettings(),
p.listensToUnpairedDevices(), p.getSupportedPacketTypes(),
p.getOutgoingPacketTypes(), p.getClass());
pluginInfo.put(p.getPluginKey(), info);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
Log.i("PluginFactory","Loaded "+pluginInfo.size()+" plugins");
}
public static @NonNull Set<String> getAvailablePlugins() {
return pluginInfo.keySet();
}
public static @Nullable Plugin instantiatePluginForDevice(Context context, String pluginKey, Device device) {
PluginInfo info = pluginInfo.get(pluginKey);
try {
Plugin plugin = info.getInstantiableClass().newInstance();
plugin.setContext(context, device);
return plugin;
} catch (Exception e) {
Log.e("PluginFactory", "Could not instantiate plugin: " + pluginKey, e);
return null;
}
}
public static @NonNull Set<String> getIncomingCapabilities() {
HashSet<String> capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getSupportedPacketTypes());
}
return capabilities;
}
public static @NonNull Set<String> getOutgoingCapabilities() {
HashSet<String> capabilities = new HashSet<>();
for (PluginInfo plugin : pluginInfo.values()) {
capabilities.addAll(plugin.getOutgoingPacketTypes());
}
return capabilities;
}
public static @NonNull Set<String> pluginsForCapabilities(Set<String> incoming, Set<String> outgoing) {
HashSet<String> plugins = new HashSet<>();
for (Map.Entry<String, PluginInfo> entry : pluginInfo.entrySet()) {
String pluginId = entry.getKey();
PluginInfo info = entry.getValue();
//Check incoming against outgoing
if (Collections.disjoint(outgoing, info.getSupportedPacketTypes())
&& Collections.disjoint(incoming, info.getOutgoingPacketTypes())) {
Log.d("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities");
continue; //No capabilities in common, do not load this plugin
}
plugins.add(pluginId);
}
return plugins;
}
}

View File

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
*/
package org.kde.kdeconnect.Plugins
import android.content.Context
import android.util.Log
import androidx.annotation.DrawableRes
import org.atteo.classindex.ClassIndex
import org.atteo.classindex.IndexAnnotated
import org.kde.kdeconnect.Device
object PluginFactory {
private var pluginInfo: Map<String, PluginInfo> = mapOf()
fun initPluginInfo(context: Context) {
try {
val plugins = ClassIndex.getAnnotated(LoadablePlugin::class.java)
.map { it.newInstance() as Plugin }
.map { plugin -> plugin.apply { setContext(context, null) } }
pluginInfo = plugins.associate { plugin -> Pair(plugin.pluginKey, PluginInfo(plugin)) }
} catch (e: Exception) {
throw RuntimeException(e)
}
Log.i("PluginFactory", "Loaded " + pluginInfo.size + " plugins")
}
val availablePlugins: Set<String>
get() = pluginInfo.keys
val incomingCapabilities: Set<String>
get() = pluginInfo.values.flatMap { plugin -> plugin.supportedPacketTypes }.toSet()
val outgoingCapabilities: Set<String>
get() = pluginInfo.values.flatMap { plugin -> plugin.outgoingPacketTypes }.toSet()
@JvmStatic
fun getPluginInfo(pluginKey: String): PluginInfo = pluginInfo[pluginKey]!!
@JvmStatic
fun sortPluginList(plugins: List<String>): List<String> {
return plugins.sortedBy { pluginInfo[it]?.displayName }
}
fun instantiatePluginForDevice(context: Context, pluginKey: String, device: Device): Plugin? {
try {
val plugin = pluginInfo[pluginKey]?.instantiableClass?.newInstance()?.apply { setContext(context, device) }
return plugin
} catch (e: Exception) {
Log.e("PluginFactory", "Could not instantiate plugin: $pluginKey", e)
return null
}
}
fun pluginsForCapabilities(incoming: Set<String>, outgoing: Set<String>): Set<String> {
fun hasCommonCapabilities(info: PluginInfo): Boolean =
outgoing.any { it in info.supportedPacketTypes } ||
incoming.any { it in info.outgoingPacketTypes }
val (used, unused) = pluginInfo.entries.partition { hasCommonCapabilities(it.value) }
for (pluginId in unused.map { it.key }) {
Log.d("PluginFactory", "Won't load $pluginId because of unmatched capabilities")
}
return used.map { it.key }.toSet()
}
@IndexAnnotated
annotation class LoadablePlugin //Annotate plugins with this so PluginFactory finds them
class PluginInfo private constructor(
val displayName: String,
val description: String,
// DrawableRes needs to be in the primary constructor, which is why we need 2 constructors
@field:DrawableRes @get:DrawableRes @param:DrawableRes val icon: Int,
val isEnabledByDefault: Boolean,
val hasSettings: Boolean,
val supportsDeviceSpecificSettings: Boolean,
val listenToUnpaired: Boolean,
supportedPacketTypes: Array<String>,
outgoingPacketTypes: Array<String>,
val instantiableClass: Class<out Plugin>,
) {
internal constructor(p: Plugin) : this(p.displayName, p.description, p.icon,
p.isEnabledByDefault, p.hasSettings(), p.supportsDeviceSpecificSettings(),
p.listensToUnpairedDevices(), p.supportedPacketTypes,
p.outgoingPacketTypes, p.javaClass)
val supportedPacketTypes: Set<String> = supportedPacketTypes.toSet()
val outgoingPacketTypes: Set<String> = outgoingPacketTypes.toSet()
}
}

View File

@ -38,7 +38,7 @@ public class PluginPreference extends SwitchPreference {
setSummary(info.getDescription());
setChecked(device.isPluginEnabled(pluginKey));
if (info.hasSettings()) {
if (info.getHasSettings()) {
this.listener = v -> {
Plugin plugin = device.getPluginIncludingWithoutPermissions(pluginKey);
if (plugin != null) {

View File

@ -83,9 +83,8 @@ public class PluginSettingsListFragment extends PreferenceFragmentCompat {
activity.runOnUiThread(activity::finish);
return;
}
List<String> plugins = device.getSupportedPlugins();
PluginFactory.sortPluginList(plugins);
List<String> plugins = PluginFactory.sortPluginList(device.getSupportedPlugins());
for (final String pluginKey : plugins) {
//TODO: Use PreferenceManagers context
PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback);