diff --git a/AndroidManifest.xml b/AndroidManifest.xml index b1e1c513..7e94b4b9 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,205 +1,214 @@ + + + + + + + diff --git a/res/drawable-hdpi/ic_keyboard_hide_white_36dp.png b/res/drawable-hdpi/ic_keyboard_hide_white_36dp.png new file mode 100644 index 00000000..d33620c4 Binary files /dev/null and b/res/drawable-hdpi/ic_keyboard_hide_white_36dp.png differ diff --git a/res/drawable-hdpi/ic_phonelink_off_white_36dp.png b/res/drawable-hdpi/ic_phonelink_off_white_36dp.png new file mode 100644 index 00000000..b57c95fa Binary files /dev/null and b/res/drawable-hdpi/ic_phonelink_off_white_36dp.png differ diff --git a/res/drawable-hdpi/ic_phonelink_white_36dp.png b/res/drawable-hdpi/ic_phonelink_white_36dp.png new file mode 100644 index 00000000..c6435eab Binary files /dev/null and b/res/drawable-hdpi/ic_phonelink_white_36dp.png differ diff --git a/res/drawable-mdpi/ic_keyboard_hide_white_36dp.png b/res/drawable-mdpi/ic_keyboard_hide_white_36dp.png new file mode 100644 index 00000000..8acf031c Binary files /dev/null and b/res/drawable-mdpi/ic_keyboard_hide_white_36dp.png differ diff --git a/res/drawable-mdpi/ic_phonelink_off_white_36dp.png b/res/drawable-mdpi/ic_phonelink_off_white_36dp.png new file mode 100644 index 00000000..ed078fc9 Binary files /dev/null and b/res/drawable-mdpi/ic_phonelink_off_white_36dp.png differ diff --git a/res/drawable-mdpi/ic_phonelink_white_36dp.png b/res/drawable-mdpi/ic_phonelink_white_36dp.png new file mode 100644 index 00000000..04f4376c Binary files /dev/null and b/res/drawable-mdpi/ic_phonelink_white_36dp.png differ diff --git a/res/drawable-xhdpi/ic_keyboard_hide_white_36dp.png b/res/drawable-xhdpi/ic_keyboard_hide_white_36dp.png new file mode 100644 index 00000000..20768d43 Binary files /dev/null and b/res/drawable-xhdpi/ic_keyboard_hide_white_36dp.png differ diff --git a/res/drawable-xhdpi/ic_phonelink_off_white_36dp.png b/res/drawable-xhdpi/ic_phonelink_off_white_36dp.png new file mode 100644 index 00000000..44d17e82 Binary files /dev/null and b/res/drawable-xhdpi/ic_phonelink_off_white_36dp.png differ diff --git a/res/drawable-xhdpi/ic_phonelink_white_36dp.png b/res/drawable-xhdpi/ic_phonelink_white_36dp.png new file mode 100644 index 00000000..31279568 Binary files /dev/null and b/res/drawable-xhdpi/ic_phonelink_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_keyboard_hide_white_36dp.png b/res/drawable-xxhdpi/ic_keyboard_hide_white_36dp.png new file mode 100644 index 00000000..89e88924 Binary files /dev/null and b/res/drawable-xxhdpi/ic_keyboard_hide_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_phonelink_off_white_36dp.png b/res/drawable-xxhdpi/ic_phonelink_off_white_36dp.png new file mode 100644 index 00000000..aa2fbc92 Binary files /dev/null and b/res/drawable-xxhdpi/ic_phonelink_off_white_36dp.png differ diff --git a/res/drawable-xxhdpi/ic_phonelink_white_36dp.png b/res/drawable-xxhdpi/ic_phonelink_white_36dp.png new file mode 100644 index 00000000..b37ea079 Binary files /dev/null and b/res/drawable-xxhdpi/ic_phonelink_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_keyboard_hide_white_36dp.png b/res/drawable-xxxhdpi/ic_keyboard_hide_white_36dp.png new file mode 100644 index 00000000..0db7049d Binary files /dev/null and b/res/drawable-xxxhdpi/ic_keyboard_hide_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_phonelink_off_white_36dp.png b/res/drawable-xxxhdpi/ic_phonelink_off_white_36dp.png new file mode 100644 index 00000000..9e47c4a0 Binary files /dev/null and b/res/drawable-xxxhdpi/ic_phonelink_off_white_36dp.png differ diff --git a/res/drawable-xxxhdpi/ic_phonelink_white_36dp.png b/res/drawable-xxxhdpi/ic_phonelink_white_36dp.png new file mode 100644 index 00000000..7806fb2b Binary files /dev/null and b/res/drawable-xxxhdpi/ic_phonelink_white_36dp.png differ diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 47c82246..31703b8e 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -1,5 +1,6 @@ 16dp 16dp + 48dip diff --git a/res/values/strings.xml b/res/values/strings.xml index 955b88d3..6259f824 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1,201 +1,208 @@ Telephony notifier Send notifications for SMS and calls Battery report Periodically report battery status Filesystem expose Allows to browse this device\'s filesystem remotely Clipboard sync Share the clipboard content Remote input Use your phone or tablet as a touchpad and keyboard + Receive remote keypresses + Receive keypress events from remote devices Multimedia controls Provides a remote control for your media player Run Command Trigger remote commands from your phone or tablet Ping Send and receive pings Notification sync Access your notifications from other devices Receive notifications Receive notifications from the other device and display them on Android Share and receive Share files and URLs between devices This feature is not available in your Android version No devices OK Cancel Open settings You need to grant permission to access notifications Send ping Multimedia control + remotekeyboard_editing_only + Handle remote keys only when editing + There is no active remote keyboard connection, establish one in kdeconnect + Remote keyboard connection is active + There is more than one remote keyboard connection, select the device to configure Remote input Move a finger on the screen to move the mouse cursor. Tap for a click, and use two/three fingers for right and middle buttons. Use a long press to drag\'n drop. Set two finger tap action Set three finger tap action Set touchpad sensitivity mousepad_double_tap_key mousepad_triple_tap_key mousepad_sensitivity_key Reverse Scrolling Direction mousepad_scroll_direction Right click Middle click Nothing right middle default right middle none Slowest Above Slowest Default Above Default Fastest slowest aboveSlowest default aboveDefault fastest Connected devices Available devices Remembered devices Plugins failed to load (tap for more info): Plugin settings Unpair Paired device not reachable Pair new device Unknown device Device not reachable Pairing already requested Device already paired Could not send package Timed out Canceled by user Canceled by other peer Invalid key received Encryption Info The other device doesn\'t use a recent version of KDE Connect, using the legacy encryption method. SHA1 fingerprint of your device certificate is: SHA1 fingerprint of remote device certificate is: Pair requested Pairing request from %1s Received link from %1s Tap to open \'%1s\' Incoming file from %1s %1s Sending file to %1s Sending files to %1s %1s Sent %1$d out of %2$d files Received file from %1s Failed receiving file from %1s Tap to open \'%1s\' Sent file to %1s %1s Failed to send file to %1s %1s Tap to answer Reconnect Send Right Click Send Middle Click Show Keyboard Device not paired Request pairing Accept Reject Device Pair device Remote control KDE Connect Settings Play Previous Rewind Fast-forward Next Volume Multimedia Settings Forward/rewind buttons Adjust the time to fast forward/rewind when pressed. mpris_interval_time 10 seconds 20 seconds 30 seconds 1 minute 2 minutes 10000000 10000000 20000000 30000000 60000000 120000000 Share To... This device uses an old protocol version This device uses a newer protocol version General Settings Settings %s settings Device name %s Invalid device name Received text, saved to clipboard Custom device list Pair a new device Unpair %s Add devices by IP Noisy notifications Vibrate and play a sound when receiving a file Customize destination directory Received files will appear in Downloads Files will be stored in the directory below Destination directory Notification filter Notifications will be synchronized for the selected apps. Internal storage All files SD card %d SD card (read only) Camera pictures Add host/IP Hostname or IP No players found Use this option only if your device is not automatically detected. Enter IP address or hostname below and touch the button to add it to the list. Touch an existing item to remove it from the list. %1$s on %2$s Send files KDE Connect Devices Other devices running KDE Connect in your same network should appear here. Device paired Rename device Rename Refresh This paired device is not reachable. Make sure it is connected to your same network. There are no file browsers installed. Send SMS Send text messages from your desktop This plugin is not supported by the device Find my phone Find my tablet Rings this device so you can find it Found Open Close diff --git a/res/xml/remotekeyboardplugin_keyboard.xml b/res/xml/remotekeyboardplugin_keyboard.xml new file mode 100644 index 00000000..b7772d03 --- /dev/null +++ b/res/xml/remotekeyboardplugin_keyboard.xml @@ -0,0 +1,14 @@ + + + + + + + + --> + + diff --git a/res/xml/remotekeyboardplugin_method.xml b/res/xml/remotekeyboardplugin_method.xml new file mode 100644 index 00000000..1e79a99a --- /dev/null +++ b/res/xml/remotekeyboardplugin_method.xml @@ -0,0 +1,4 @@ + + + diff --git a/res/xml/remotekeyboardplugin_preferences.xml b/res/xml/remotekeyboardplugin_preferences.xml new file mode 100644 index 00000000..28b99a21 --- /dev/null +++ b/res/xml/remotekeyboardplugin_preferences.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java index 1da88ef2..b514634a 100644 --- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java +++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java @@ -1,220 +1,222 @@ /* * Copyright 2014 Albert Vaca Cintora * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License or (at your option) version 3 or any later version * accepted by the membership of KDE e.V. (or its successor approved * by the membership of KDE e.V.), which shall act as a proxy * defined in Section 14 of version 3 of the license. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ package org.kde.kdeconnect.Plugins; import android.content.Context; import android.graphics.drawable.Drawable; import android.util.Log; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin; import org.kde.kdeconnect.Plugins.ClibpoardPlugin.ClipboardPlugin; import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin; import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin; import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin; import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin; import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin; import org.kde.kdeconnect.Plugins.ReceiveNotificationsPlugin.ReceiveNotificationsPlugin; +import org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardPlugin; import org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandPlugin; import org.kde.kdeconnect.Plugins.SftpPlugin.SftpPlugin; import org.kde.kdeconnect.Plugins.SharePlugin.SharePlugin; import org.kde.kdeconnect.Plugins.TelepathyPlugin.TelepathyPlugin; import org.kde.kdeconnect.Plugins.TelephonyPlugin.TelephonyPlugin; import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.TreeMap; public class PluginFactory { public static class PluginInfo { public PluginInfo(String displayName, String description, Drawable icon, boolean enabledByDefault, boolean hasSettings, boolean listenToUnpaired, String[] supportedPackageTypes, String[] outgoingPackageTypes) { this.displayName = displayName; this.description = description; this.icon = icon; this.enabledByDefault = enabledByDefault; this.hasSettings = hasSettings; this.listenToUnpaired = listenToUnpaired; HashSet incoming = new HashSet<>(); if (supportedPackageTypes != null) Collections.addAll(incoming, supportedPackageTypes); this.supportedPackageTypes = Collections.unmodifiableSet(incoming); HashSet outgoing = new HashSet<>(); if (outgoingPackageTypes != null) Collections.addAll(outgoing, outgoingPackageTypes); this.outgoingPackageTypes = Collections.unmodifiableSet(outgoing); } public String getDisplayName() { return displayName; } public String getDescription() { return description; } public Drawable getIcon() { return icon; } public boolean hasSettings() { return hasSettings; } public boolean isEnabledByDefault() { return enabledByDefault; } public boolean listenToUnpaired() { return listenToUnpaired; } public Set getOutgoingPackageTypes() { return outgoingPackageTypes; } public Set getSupportedPackageTypes() { return supportedPackageTypes; } private final String displayName; private final String description; private final Drawable icon; private final boolean enabledByDefault; private final boolean hasSettings; private final boolean listenToUnpaired; private final Set supportedPackageTypes; private final Set outgoingPackageTypes; } private static final Map availablePlugins = new TreeMap<>(); private static final Map pluginInfoCache = new TreeMap<>(); static { PluginFactory.registerPlugin(TelephonyPlugin.class); PluginFactory.registerPlugin(PingPlugin.class); PluginFactory.registerPlugin(MprisPlugin.class); PluginFactory.registerPlugin(ClipboardPlugin.class); PluginFactory.registerPlugin(BatteryPlugin.class); PluginFactory.registerPlugin(SftpPlugin.class); PluginFactory.registerPlugin(NotificationsPlugin.class); PluginFactory.registerPlugin(ReceiveNotificationsPlugin.class); PluginFactory.registerPlugin(MousePadPlugin.class); PluginFactory.registerPlugin(SharePlugin.class); PluginFactory.registerPlugin(TelepathyPlugin.class); PluginFactory.registerPlugin(FindMyPhonePlugin.class); PluginFactory.registerPlugin(RunCommandPlugin.class); + PluginFactory.registerPlugin(RemoteKeyboardPlugin.class); } public static PluginInfo getPluginInfo(Context context, String pluginKey) { PluginInfo info = pluginInfoCache.get(pluginKey); //Is it cached? if (info != null) { return info; } try { Plugin p = ((Plugin)availablePlugins.get(pluginKey).newInstance()); p.setContext(context, null); info = new PluginInfo(p.getDisplayName(), p.getDescription(), p.getIcon(), p.isEnabledByDefault(), p.hasSettings(), p.listensToUnpairedDevices(), p.getSupportedPackageTypes(), p.getOutgoingPackageTypes()); pluginInfoCache.put(pluginKey, info); //Cache it return info; } catch(Exception e) { Log.e("PluginFactory","getPluginInfo exception"); e.printStackTrace(); return null; } } public static Set getAvailablePlugins() { return availablePlugins.keySet(); } public static Plugin instantiatePluginForDevice(Context context, String pluginKey, Device device) { Class c = availablePlugins.get(pluginKey); if (c == null) { Log.e("PluginFactory", "Plugin not found: "+pluginKey); return null; } try { Plugin plugin = (Plugin)c.newInstance(); plugin.setContext(context, device); return plugin; } catch(Exception e) { Log.e("PluginFactory", "Could not instantiate plugin: "+pluginKey); e.printStackTrace(); return null; } } public static void registerPlugin(Class pluginClass) { try { String pluginKey = Plugin.getPluginKey(pluginClass); availablePlugins.put(pluginKey, pluginClass); } catch(Exception e) { Log.e("PluginFactory","addPlugin exception"); e.printStackTrace(); } } public static Set getIncomingCapabilities(Context context) { HashSet capabilities = new HashSet<>(); for (String pluginId : availablePlugins.keySet()) { PluginInfo plugin = getPluginInfo(context, pluginId); capabilities.addAll(plugin.getSupportedPackageTypes()); } return capabilities; } public static Set getOutgoingCapabilities(Context context) { HashSet capabilities = new HashSet<>(); for (String pluginId : availablePlugins.keySet()) { PluginInfo plugin = getPluginInfo(context, pluginId); capabilities.addAll(plugin.getOutgoingPackageTypes()); } return capabilities; } public static Set pluginsForCapabilities(Context context, Set incoming, Set outgoing) { HashSet plugins = new HashSet<>(); for (String pluginId : availablePlugins.keySet()) { PluginInfo plugin = getPluginInfo(context, pluginId); //Check incoming against outgoing if (Collections.disjoint(outgoing, plugin.getSupportedPackageTypes()) && Collections.disjoint(incoming, plugin.getOutgoingPackageTypes())) { Log.i("PluginFactory", "Won't load " + pluginId + " because of unmatched capabilities"); continue; //No capabilities in common, do not load this plugin } plugins.add(pluginId); } return plugins; } } diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java new file mode 100644 index 00000000..1598e260 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java @@ -0,0 +1,398 @@ +/* + * Copyright 2017 Holger Kaelberer + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin; + +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.SystemClock; +import android.preference.PreferenceManager; +import android.support.v4.content.ContextCompat; +import android.support.v4.util.Pair; +import android.util.Log; +import android.util.SparseIntArray; +import android.view.KeyEvent; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.InputConnection; + +import org.kde.kdeconnect.NetworkPackage; +import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.concurrent.locks.ReentrantLock; + +public class RemoteKeyboardPlugin extends Plugin { + + public final static String PACKAGE_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request"; + public final static String PACKAGE_TYPE_MOUSEPAD_ECHO = "kdeconnect.mousepad.echo"; + public final static String PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE = "kdeconnect.mousepad.keyboardstate"; + + /** + * Track and expose plugin instances to allow for a 'connected'-indicator in the IME: + */ + private static ArrayList instances = new ArrayList(); + private static ReentrantLock instancesLock = new ReentrantLock(true); + public static ArrayList getInstances() { + return instances; + } + public static ArrayList acquireInstances() { + instancesLock.lock(); + return getInstances(); + } + public static ArrayList releaseInstances() { + instancesLock.unlock(); + return getInstances(); + } + public static boolean isConnected() { + return instances.size() > 0; + } + + private static SparseIntArray specialKeyMap = new SparseIntArray(); + + static { + int i = 0; + specialKeyMap.put(++i, KeyEvent.KEYCODE_DEL); // 1 + specialKeyMap.put(++i, KeyEvent.KEYCODE_TAB); // 2 + ++i; //specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER, 12); // 3 is not used + specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_LEFT); // 4 + specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_UP); // 5 + specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_RIGHT); // 6 + specialKeyMap.put(++i, KeyEvent.KEYCODE_DPAD_DOWN); // 7 + specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_UP); // 8 + specialKeyMap.put(++i, KeyEvent.KEYCODE_PAGE_DOWN); // 9 + if (Build.VERSION.SDK_INT >= 11) { + specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_HOME); // 10 + specialKeyMap.put(++i, KeyEvent.KEYCODE_MOVE_END); // 11 + specialKeyMap.put(++i, KeyEvent.KEYCODE_ENTER); // 12 + specialKeyMap.put(++i, KeyEvent.KEYCODE_FORWARD_DEL); // 13 + specialKeyMap.put(++i, KeyEvent.KEYCODE_ESCAPE); // 14 + specialKeyMap.put(++i, KeyEvent.KEYCODE_SYSRQ); // 15 + specialKeyMap.put(++i, KeyEvent.KEYCODE_SCROLL_LOCK); // 16 + ++i; // 17 + ++i; // 18 + ++i; // 19 + ++i; // 20 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F1); // 21 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F2); // 22 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F3); // 23 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F4); // 24 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F5); // 25 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F6); // 26 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F7); // 27 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F8); // 28 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F9); // 29 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F10); // 30 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F11); // 31 + specialKeyMap.put(++i, KeyEvent.KEYCODE_F12); // 21 + } + } + + @Override + public boolean onCreate() { + Log.d("RemoteKeyboardPlugin", "Creating for device " + device.getName()); + acquireInstances(); + try { + instances.add(this); + } finally { + releaseInstances(); + } + if (RemoteKeyboardService.instance != null) + RemoteKeyboardService.instance.handler.post(new Runnable() { + @Override + public void run() { + RemoteKeyboardService.instance.updateInputView(); + } + }); + return true; + } + + @Override + public void onDestroy() { + acquireInstances(); + try { + if (instances.contains(this)) { + instances.remove(this); + if (instances.size() < 1 && RemoteKeyboardService.instance != null) + RemoteKeyboardService.instance.handler.post(new Runnable() { + @Override + public void run() { + RemoteKeyboardService.instance.updateInputView(); + } + }); + } + } finally { + releaseInstances(); + } + + Log.d("RemoteKeyboardPlugin", "Destroying for device " + device.getName()); + } + + @Override + public String getDisplayName() { + return context.getString(R.string.pref_plugin_remotekeyboard); + } + + @Override + public String getDescription() { + return context.getString(R.string.pref_plugin_remotekeyboard_desc); + } + + @Override + public Drawable getIcon() { + return ContextCompat.getDrawable(context, R.drawable.ic_action_keyboard); + } + + @Override + public boolean hasSettings() { + return true; + } + + @Override + public boolean hasMainActivity() { + return false; + } + + @Override + public String[] getSupportedPackageTypes() { + return new String[]{PACKAGE_TYPE_MOUSEPAD_REQUEST}; + } + + @Override + public String[] getOutgoingPackageTypes() { + return new String[]{PACKAGE_TYPE_MOUSEPAD_ECHO, PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE}; + } + + private boolean isValidSpecialKey(int key) { + return (specialKeyMap.get(key, 0) > 0); + } + + private int getCharPos(ExtractedText extractedText, char ch, boolean forward) { + int pos = -1; + if (extractedText != null) { + if (!forward) // backward + pos = extractedText.text.toString().lastIndexOf(" ", extractedText.selectionEnd - 2); + else + pos = extractedText.text.toString().indexOf(" ", extractedText.selectionEnd + 1); + return pos; + } + return pos; + } + + private int currentTextLength(ExtractedText extractedText) { + if (extractedText != null) + return extractedText.text.length(); + return -1; + } + + private int currentCursorPos(ExtractedText extractedText) { + if (extractedText != null) + return extractedText.selectionEnd; + return -1; + } + + private Pair currentSelection(ExtractedText extractedText) { + if (extractedText != null) + return new Pair<>(extractedText.selectionStart, extractedText.selectionEnd); + return new Pair<>(-1, -1); + } + + private boolean handleSpecialKey(int key, boolean shift, boolean ctrl, boolean alt) { + int keyEvent = specialKeyMap.get(key, 0); + if (keyEvent == 0) + return false; + InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); +// Log.d("RemoteKeyboardPlugin", "Handling special key " + key + " translated to " + keyEvent + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt); + + // special sequences: + if (ctrl && (keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT)) { + // Ctrl + right -> next word + ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); + int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); + if (pos == -1) + pos = currentTextLength(extractedText); + else + pos++; + int startPos = pos; + int endPos = pos; + if (shift) { // Shift -> select word (otherwise jump) + Pair sel = currentSelection(extractedText); + int cursor = currentCursorPos(extractedText); +// Log.d("RemoteKeyboardPlugin", "Selection (to right): " + sel.first + " / " + sel.second + " cursor: " + cursor); + startPos = cursor; + if (sel.first < cursor || // active selection from left to right -> grow + sel.first > sel.second) // active selection from right to left -> shrink + startPos = sel.first; + } + inputConn.setSelection(startPos, endPos); + } else if (ctrl && keyEvent == KeyEvent.KEYCODE_DPAD_LEFT) { + // Ctrl + left -> previous word + ExtractedText extractedText = inputConn.getExtractedText(new ExtractedTextRequest(), 0); + int pos = getCharPos(extractedText, ' ', keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT); + if (pos == -1) + pos = 0; + else + pos++; + int startPos = pos; + int endPos = pos; + if (shift) { + Pair sel = currentSelection(extractedText); + int cursor = currentCursorPos(extractedText); +// Log.d("RemoteKeyboardPlugin", "Selection (to left): " + sel.first + " / " + sel.second + " cursor: " + cursor); + startPos = cursor; + if (cursor < sel.first || // active selection from right to left -> grow + sel.first < sel.second) // active selection from right to left -> shrink + startPos = sel.first; + } + inputConn.setSelection(startPos, endPos); + } else if (shift + && (keyEvent == KeyEvent.KEYCODE_DPAD_LEFT + || keyEvent == KeyEvent.KEYCODE_DPAD_RIGHT + || keyEvent == KeyEvent.KEYCODE_DPAD_UP + || keyEvent == KeyEvent.KEYCODE_DPAD_DOWN + || keyEvent == KeyEvent.KEYCODE_MOVE_HOME + || keyEvent == KeyEvent.KEYCODE_MOVE_END)) { + // Shift + up/down/left/right/home/end + long now = SystemClock.uptimeMillis(); + inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); + inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); + inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, keyEvent, 0, KeyEvent.META_SHIFT_LEFT_ON)); + inputConn.sendKeyEvent(new KeyEvent(now, now, KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SHIFT_LEFT, 0, 0)); + } else if (keyEvent == KeyEvent.KEYCODE_NUMPAD_ENTER + || keyEvent == KeyEvent.KEYCODE_ENTER) { + // Enter key + EditorInfo editorInfo = RemoteKeyboardService.instance.getCurrentInputEditorInfo(); +// Log.d("RemoteKeyboardPlugin", "Enter: " + editorInfo.imeOptions); + if (editorInfo != null + && (((editorInfo.imeOptions & EditorInfo.IME_FLAG_NO_ENTER_ACTION) == 0) + || ctrl)) { // Ctrl+Return overrides IME_FLAG_NO_ENTER_ACTION (FIXME: make configurable?) + // check for special DONE/GO/etc actions first: + int[] actions = { EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_NEXT, + EditorInfo.IME_ACTION_SEND, EditorInfo.IME_ACTION_SEARCH, + EditorInfo.IME_ACTION_DONE}; // note: DONE should be last or we might hide the ime instead of "go" + for (int i = 0; i < actions.length; i++) { + if ((editorInfo.imeOptions & actions[i]) == actions[i]) { +// Log.d("RemoteKeyboardPlugin", "Enter-action: " + actions[i]); + inputConn.performEditorAction(actions[i]); + return true; + } + } + } else { + // else: fall back to regular Enter-event: +// Log.d("RemoteKeyboardPlugin", "Enter: normal keypress"); + inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); + inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); + } + } else { + // default handling: + inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, keyEvent)); + inputConn.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, keyEvent)); + } + + return true; + } + + private boolean handleVisibleKey(String key, boolean shift, boolean ctrl, boolean alt) { +// Log.d("RemoteKeyboardPlugin", "Handling visible key " + key + " shift=" + shift + " ctrl=" + ctrl + " alt=" + alt + " " + key.equalsIgnoreCase("c") + " " + key.length()); + + if (key.isEmpty()) + return false; + + InputConnection inputConn = RemoteKeyboardService.instance.getCurrentInputConnection(); + if (inputConn == null) + return false; + + // ctrl+c/v/x + if (key.equalsIgnoreCase("c") && ctrl) { + return inputConn.performContextMenuAction(android.R.id.copy); + } else if (key.equalsIgnoreCase("v") && ctrl) + return inputConn.performContextMenuAction(android.R.id.paste); + else if (key.equalsIgnoreCase("x") && ctrl) + return inputConn.performContextMenuAction(android.R.id.cut); + else if (key.equalsIgnoreCase("a") && ctrl) + return inputConn.performContextMenuAction(android.R.id.selectAll); + +// Log.d("RemoteKeyboardPlugin", "Committing visible key '" + key + "'"); + inputConn.commitText(key, key.length()); + return true; + } + + private boolean handleEvent(NetworkPackage np) { + if (np.has("specialKey") && isValidSpecialKey(np.getInt("specialKey"))) + return handleSpecialKey(np.getInt("specialKey"), np.getBoolean("shift"), + np.getBoolean("ctrl"), np.getBoolean("alt")); + + // try visible key + return handleVisibleKey(np.getString("key"), np.getBoolean("shift"), + np.getBoolean("ctrl"), np.getBoolean("alt")); + } + + @Override + public boolean onPackageReceived(NetworkPackage np) { + + if (!np.getType().equals(PACKAGE_TYPE_MOUSEPAD_REQUEST) + || (!np.has("key") && !np.has("specialKey"))) { // expect at least key OR specialKey + Log.e("RemoteKeyboardPlugin", "Invalid package for remotekeyboard plugin!"); + return false; + } + + if (RemoteKeyboardService.instance == null) { + Log.i("RemoteKeyboardPlugin", "Remote keyboard is not the currently selected input method, dropping key"); + return false; + } + + if (!RemoteKeyboardService.instance.visible && + PreferenceManager.getDefaultSharedPreferences(context).getBoolean(context.getString(R.string.remotekeyboard_editing_only), true)) { + Log.i("RemoteKeyboardPlugin", "Remote keyboard is currently not visible, dropping key"); + return false; + } + + if (!handleEvent(np)) { + Log.i("RemoteKeyboardPlugin", "Could not handle event!"); + return false; + } + + if (np.getBoolean("sendAck")) { + NetworkPackage reply = new NetworkPackage(PACKAGE_TYPE_MOUSEPAD_ECHO); + reply.set("key", np.getString("key")); + if (np.has("specialKey")) + reply.set("specialKey", np.getInt("specialKey")); + if (np.has("shift")) + reply.set("shift", np.getBoolean("shift")); + if (np.has("ctrl")) + reply.set("ctrl", np.getBoolean("ctrl")); + if (np.has("alt")) + reply.set("alt", np.getBoolean("alt")); + reply.set("isAck", true); + device.sendPackage(reply); + } + + return true; + } + + public void notifyKeyboardState(boolean state) { + Log.d("RemoteKeyboardPlugin", "Keyboardstate changed to " + state); + NetworkPackage np = new NetworkPackage(PACKAGE_TYPE_MOUSEPAD_KEYBOARDSTATE); + np.set("state", state); + device.sendPackage(np); + } +} diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java new file mode 100644 index 00000000..a118226e --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java @@ -0,0 +1,233 @@ +/* + * Copyright 2017 Holger Kaelberer + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License as + * published by the Free Software Foundation; either version 2 of + * the License or (at your option) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin; + +import android.content.Context; +import android.content.Intent; +import android.inputmethodservice.InputMethodService; +import android.inputmethodservice.Keyboard; +import android.inputmethodservice.KeyboardView; +import android.inputmethodservice.KeyboardView.OnKeyboardActionListener; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Toast; + +import org.kde.kdeconnect.UserInterface.MaterialActivity; +import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; +import org.kde.kdeconnect_tp.R; + +import java.util.ArrayList; +import java.util.List; + +public class RemoteKeyboardService + extends InputMethodService + implements OnKeyboardActionListener { + + /** + * Reference to our instance + * null if this InputMethod is not currently selected. + */ + public static RemoteKeyboardService instance = null; + + /** + * Whether input is currently accepted + * Implies visible == true + */ + public boolean active = false; + + /** + * Whether this InputMethod is currently visible. + */ + public boolean visible = false; + + KeyboardView inputView = null; + private final int StatusKeyIdx = 3; + private final int connectedIcon = R.drawable.ic_phonelink_white_36dp; + private final int disconnectedIcon = R.drawable.ic_phonelink_off_white_36dp; + + Handler handler; + + void updateInputView() { + if (inputView == null) + return; + Keyboard currentKeyboard = inputView.getKeyboard(); + List keys = currentKeyboard.getKeys(); + boolean connected = RemoteKeyboardPlugin.isConnected(); +// Log.d("RemoteKeyboardService", "Updating keyboard connection icon, connected=" + connected); + keys.get(StatusKeyIdx).icon = getResources().getDrawable(connected ? connectedIcon : disconnectedIcon); + inputView.invalidateKey(StatusKeyIdx); + } + + @Override + public void onCreate() { + super.onCreate(); + active = false; + visible = false; + instance = this; + handler = new Handler(); + Log.d("RemoteKeyboardService", "Remote keyboard initialized"); + } + + @Override + public void onDestroy() { + super.onDestroy(); + instance = null; + Log.d("RemoteKeyboardService", "Destroyed"); + } + + @Override + public void onInitializeInterface() { + super.onInitializeInterface(); + } + + @Override + public View onCreateInputView() { +// Log.d("RemoteKeyboardService", "onCreateInputView connected=" + RemoteKeyboardPlugin.isConnected()); + inputView = new KeyboardView(this, null); + inputView.setKeyboard(new Keyboard(this, R.xml.remotekeyboardplugin_keyboard)); + inputView.setPreviewEnabled(false); + inputView.setOnKeyboardActionListener(this); + updateInputView(); + return inputView; + } + + @Override + public void onStartInputView(EditorInfo attribute, boolean restarting) { +// Log.d("RemoteKeyboardService", "onStartInputView"); + super.onStartInputView(attribute, restarting); + visible = true; + ArrayList instances = RemoteKeyboardPlugin.acquireInstances(); + try { + for (RemoteKeyboardPlugin i: instances) + i.notifyKeyboardState(true); + } finally { + RemoteKeyboardPlugin.releaseInstances(); + } + } + + @Override + public void onFinishInputView(boolean finishingInput) { +// Log.d("RemoteKeyboardService", "onFinishInputView"); + super.onFinishInputView(finishingInput); + visible = false; + ArrayList instances = RemoteKeyboardPlugin.acquireInstances(); + try { + for (RemoteKeyboardPlugin i: instances) + i.notifyKeyboardState(false); + } finally { + RemoteKeyboardPlugin.releaseInstances(); + } + } + + @Override + public void onStartInput(EditorInfo attribute, boolean restarting) { +// Log.d("RemoteKeyboardService", "onStartInput"); + super.onStartInput(attribute, restarting); + active = true; + } + + @Override + public void onFinishInput() { +// Log.d("RemoteKeyboardService", "onFinishInput"); + super.onFinishInput(); + active = false; + } + + @Override + public void onPress(int primaryCode) { + switch (primaryCode) { + case 0: { // "hide keyboard" + requestHideSelf(0); + break; + } + case 1: { // "settings" + ArrayList instances = RemoteKeyboardPlugin.acquireInstances(); + try { + if (instances.size() == 1) { // single instance of RemoteKeyboardPlugin -> access its settings + RemoteKeyboardPlugin plugin = instances.get(0); + if (plugin != null) { + Intent intent = new Intent(this, PluginSettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("plugin_display_name", plugin.getDisplayName()); + intent.putExtra("plugin_key", plugin.getPluginKey()); + startActivity(intent); + } + } else { // != 1 instance of plugin -> show main activity view + Intent intent = new Intent(this, MaterialActivity.class); + intent.putExtra("forceOverview", true); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + if (instances.size() < 1) + Toast.makeText(this, R.string.remotekeyboard_not_connected, Toast.LENGTH_SHORT).show(); + else // instances.size() > 1 + Toast.makeText(this, R.string.remotekeyboard_multiple_connections, Toast.LENGTH_SHORT).show(); + } + } finally { + RemoteKeyboardPlugin.releaseInstances(); + } + break; + } + case 2: { // "keyboard" + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showInputMethodPicker(); + break; + } + case 3: { // "connected"? + if (RemoteKeyboardPlugin.isConnected()) + Toast.makeText(this, R.string.remotekeyboard_connected, Toast.LENGTH_SHORT).show(); + else + Toast.makeText(this, R.string.remotekeyboard_not_connected, Toast.LENGTH_SHORT).show(); + break; + } + } + } + + @Override + public void onKey(int primaryCode, int[] keyCodes) { + } + + @Override + public void onText(CharSequence text) { + } + + @Override + public void swipeRight() { + } + + @Override + public void swipeLeft() { + } + + @Override + public void swipeDown() { + } + + @Override + public void swipeUp() { + } + + @Override + public void onRelease(int primaryCode) { + } +} diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/THANKS b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/THANKS new file mode 100644 index 00000000..7c623ed8 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/THANKS @@ -0,0 +1,3 @@ +Besides the official Android examples about implementing IMEs I learned a lot about using +the Android APIs around InputConnection from the excellent "Remote Keyboard" app from onyxbits +(http://www.onyxbits.de/remotekeyboard). Thanks! diff --git a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java index c28e38c6..7c35baa4 100644 --- a/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MaterialActivity.java @@ -1,292 +1,295 @@ package org.kde.kdeconnect.UserInterface; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.NavigationView; import android.support.v4.app.Fragment; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarDrawerToggle; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.Toolbar; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.EditText; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect_tp.R; import java.util.Collection; import java.util.HashMap; public class MaterialActivity extends AppCompatActivity { private static final String STATE_SELECTED_DEVICE = "selected_device"; public static final int RESULT_NEEDS_RELOAD = Activity.RESULT_FIRST_USER; private NavigationView mNavigationView; private DrawerLayout mDrawerLayout; private String mCurrentDevice; private SharedPreferences preferences; private final HashMap mMapMenuToDeviceId = new HashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); mNavigationView = (NavigationView) findViewById(R.id.navigation_drawer); View mDrawerHeader = mNavigationView.getHeaderView(0); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); ActionBarDrawerToggle mDrawerToggle = new ActionBarDrawerToggle(this, /* host Activity */ mDrawerLayout, /* DrawerLayout object */ R.string.open, /* "open drawer" description */ R.string.close /* "close drawer" description */ ); mDrawerLayout.setDrawerListener(mDrawerToggle); mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); actionBar.setDisplayHomeAsUpEnabled(true); mDrawerToggle.setDrawerIndicatorEnabled(true); mDrawerToggle.syncState(); String deviceName = DeviceHelper.getDeviceName(this); TextView nameView = (TextView) mDrawerHeader.findViewById(R.id.device_name); nameView.setText(deviceName); View.OnClickListener renameListener = new View.OnClickListener() { @Override public void onClick(View v) { renameDevice(); } }; mDrawerHeader.findViewById(R.id.kdeconnect_label).setOnClickListener(renameListener); mDrawerHeader.findViewById(R.id.device_name).setOnClickListener(renameListener); mNavigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { @Override public boolean onNavigationItemSelected(MenuItem menuItem) { String deviceId = mMapMenuToDeviceId.get(menuItem); onDeviceSelected(deviceId); mDrawerLayout.closeDrawer(mNavigationView); return true; } }); preferences = getSharedPreferences(STATE_SELECTED_DEVICE, Context.MODE_PRIVATE); String savedDevice; - if (getIntent().hasExtra("deviceId")) { + if (getIntent().hasExtra("forceOverview")) { + Log.i("MaterialActivity", "Requested to start main overview"); + savedDevice = null; + } else if (getIntent().hasExtra("deviceId")) { Log.i("MaterialActivity", "Loading selected device from parameter"); savedDevice = getIntent().getStringExtra("deviceId"); } else if (savedInstanceState != null) { Log.i("MaterialActivity", "Loading selected device from saved activity state"); savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); } else { Log.i("MaterialActivity", "Loading selected device from persistent storage"); savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null); } onDeviceSelected(savedDevice); } @Override public void onBackPressed() { if (mDrawerLayout.isDrawerOpen(mNavigationView)) { mDrawerLayout.closeDrawer(mNavigationView); } else { super.onBackPressed(); } } @Override public boolean onOptionsItemSelected(MenuItem item) { if (item.getItemId() == android.R.id.home) { mDrawerLayout.openDrawer(mNavigationView); return true; } else { return super.onOptionsItemSelected(item); } } private void updateComputerList() { //Log.e("MaterialActivity", "UpdateComputerList"); BackgroundService.RunCommand(MaterialActivity.this, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(final BackgroundService service) { Menu menu = mNavigationView.getMenu(); menu.clear(); mMapMenuToDeviceId.clear(); int id = 0; Collection devices = service.getDevices().values(); for (Device device : devices) { if (device.isReachable() && device.isPaired()) { MenuItem item = menu.add(0, id++, 0, device.getName()); item.setIcon(device.getIcon()); item.setCheckable(true); item.setChecked(device.getDeviceId().equals(mCurrentDevice)); mMapMenuToDeviceId.put(item, device.getDeviceId()); } } MenuItem item = menu.add(99, id++, 0, R.string.pair_new_device); item.setIcon(R.drawable.ic_action_content_add_circle_outline); item.setCheckable(true); item.setChecked(mCurrentDevice == null); mMapMenuToDeviceId.put(item, null); } }); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this, true); BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { service.addDeviceListChangedCallback("MaterialActivity", new BackgroundService.DeviceListChangedCallback() { @Override public void onDeviceListChanged() { updateComputerList(); } }); } }); updateComputerList(); } @Override protected void onStop() { BackgroundService.removeGuiInUseCounter(this); BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { service.removeDeviceListChangedCallback("MaterialActivity"); } }); super.onStop(); } //TODO: Make it accept two parameters, a constant with the type of screen and the device id in //case the screen is for a device, or even three parameters and the third one be the plugin id? //This way we can keep adding more options with null device id (eg: about, help...) public void onDeviceSelected(String deviceId, boolean stack) { mCurrentDevice = deviceId; preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply(); for (HashMap.Entry entry : mMapMenuToDeviceId.entrySet()) { boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe entry.getKey().setChecked(selected); } Fragment fragment; if (deviceId == null) { fragment = new PairingFragment(); } else { fragment = new DeviceFragment(deviceId, stack); } getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment) .commit(); } public void onDeviceSelected(String deviceId) { onDeviceSelected(deviceId, false); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice); } @Override protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); String savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); onDeviceSelected(savedDevice); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case RESULT_NEEDS_RELOAD: BackgroundService.RunCommand(this, new BackgroundService.InstanceCallback() { @Override public void onServiceStart(BackgroundService service) { Device device = service.getDevice(mCurrentDevice); device.reloadPluginsFromSettings(); } }); break; default: super.onActivityResult(requestCode, resultCode, data); } } public void renameDevice() { final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name); final EditText deviceNameEdit = new EditText(MaterialActivity.this); String deviceName = DeviceHelper.getDeviceName(MaterialActivity.this); deviceNameEdit.setText(deviceName); deviceNameEdit.setPadding( ((int) (18 * getResources().getDisplayMetrics().density)), ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (18 * getResources().getDisplayMetrics().density)), ((int) (12 * getResources().getDisplayMetrics().density)) ); new AlertDialog.Builder(MaterialActivity.this) .setView(deviceNameEdit) .setPositiveButton(R.string.device_rename_confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String deviceName = deviceNameEdit.getText().toString(); DeviceHelper.setDeviceName(MaterialActivity.this, deviceName); nameView.setText(deviceName); } }) .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { } }) .setTitle(R.string.device_rename_title) .show(); } }