diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index c7ba3fce..1ffdaaf0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -1,270 +1,280 @@
+
+
+
+
+
diff --git a/res/drawable-hdpi/ic_presenter.png b/res/drawable-hdpi/ic_presenter.png
new file mode 100644
index 00000000..0cb547e3
Binary files /dev/null and b/res/drawable-hdpi/ic_presenter.png differ
diff --git a/res/drawable-mdpi/ic_presenter.png b/res/drawable-mdpi/ic_presenter.png
new file mode 100644
index 00000000..749ac4a1
Binary files /dev/null and b/res/drawable-mdpi/ic_presenter.png differ
diff --git a/res/drawable-xhdpi/ic_presenter.png b/res/drawable-xhdpi/ic_presenter.png
new file mode 100644
index 00000000..44d32cf5
Binary files /dev/null and b/res/drawable-xhdpi/ic_presenter.png differ
diff --git a/res/drawable-xxhdpi/ic_presenter.png b/res/drawable-xxhdpi/ic_presenter.png
new file mode 100644
index 00000000..0546b0d8
Binary files /dev/null and b/res/drawable-xxhdpi/ic_presenter.png differ
diff --git a/res/layout/activity_presenter.xml b/res/layout/activity_presenter.xml
new file mode 100644
index 00000000..19cbda1f
--- /dev/null
+++ b/res/layout/activity_presenter.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/menu/menu_presenter.xml b/res/menu/menu_presenter.xml
new file mode 100644
index 00000000..ec7f18f7
--- /dev/null
+++ b/res/menu/menu_presenter.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f3294432..9c5c2bc0 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1,262 +1,268 @@
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
+ Presentation remote
+ Use your device to change slides in a presentation
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
Contacts Synchronizer
Allow synchronizing the device\'s contacts book
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
To be able to control your media players you need to grant access to the 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 2 fingers to scroll. 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
- Sent %1$d file
- 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
Pause
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
Show media control notification
Allows controlling your media players without opening KDE Connect.
mpris_notification_enabled
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
Share
Share \"%s\"
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.
It looks like you are on a mobile data connection. KDE Connect only works on local networks.
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
Find my TV
Rings this device so you can find it
Found
Open
Close
You need to grant permissions to access the storage
Some Plugins need permissions to work (tap for more info):
This plugin needs permissions to work
You need to grant extra permissions to enable all functions
Some plugins have features disabled because of lack of permission (tap for more info):
To access your files from your PC the app needs permission to access your phone\'s storage
To share files between your phone and your desktop you need to give access to the phone\'s storage
To read and write SMS from your desktop you need to give permission to SMS
To see phone calls and SMS from the desktop you need to give permission to phone calls and SMS
To see a contact name instead of a phone number you need to give access to the phone\'s contacts
To share your contacts book with the desktop, you need to give contacts permission
Select a ringtone
Blocked numbers
Don\'t show calls and SMS from these numbers. Please specify one number per line
Cover art of current media
Device icon
Settings icon
+ Fullscreen
+ Exit presentation
+ You can lock your device to use the volume keys as previous/next buttons
+
Add a command
There are no commands registered
You can add new commands in the KDE Connect System Settings
You can add commands on the desktop
Media Player Control
Control your phones media players from another device
Dark theme
Stop the current player
Copy URL to clipboard
Copied to clipboard
Device is not reachable
Device is not paired
There is no such device
This device does not have the Run Command Plugin enabled
Find remote device
Ring your remote device
Ring
System volume
Control the system volume of the remote device
Mute
diff --git a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java
index 371353d4..cb97c1e1 100644
--- a/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java
+++ b/src/org/kde/kdeconnect/Plugins/MousePadPlugin/KeyListenerView.java
@@ -1,175 +1,175 @@
/*
* Copyright 2014 Saikrishna Arcot
*
* 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.MousePadPlugin;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.util.SparseIntArray;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
public class KeyListenerView extends View {
private String deviceId;
- private static SparseIntArray SpecialKeysMap = new SparseIntArray();
+ public static SparseIntArray SpecialKeysMap = new SparseIntArray();
static {
int i = 0;
SpecialKeysMap.put(KeyEvent.KEYCODE_DEL, ++i); // 1
SpecialKeysMap.put(KeyEvent.KEYCODE_TAB, ++i); // 2
SpecialKeysMap.put(KeyEvent.KEYCODE_ENTER, 12);
++i; // 3 is not used, return is 12 instead
SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_LEFT, ++i); // 4
SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_UP, ++i); // 5
SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_RIGHT, ++i); // 6
SpecialKeysMap.put(KeyEvent.KEYCODE_DPAD_DOWN, ++i); // 7
SpecialKeysMap.put(KeyEvent.KEYCODE_PAGE_UP, ++i); // 8
SpecialKeysMap.put(KeyEvent.KEYCODE_PAGE_DOWN, ++i); // 9
if (Build.VERSION.SDK_INT >= 11) {
SpecialKeysMap.put(KeyEvent.KEYCODE_MOVE_HOME, ++i); // 10
SpecialKeysMap.put(KeyEvent.KEYCODE_MOVE_END, ++i); // 11
SpecialKeysMap.put(KeyEvent.KEYCODE_NUMPAD_ENTER, ++i); // 12
SpecialKeysMap.put(KeyEvent.KEYCODE_FORWARD_DEL, ++i); // 13
SpecialKeysMap.put(KeyEvent.KEYCODE_ESCAPE, ++i); // 14
SpecialKeysMap.put(KeyEvent.KEYCODE_SYSRQ, ++i); // 15
SpecialKeysMap.put(KeyEvent.KEYCODE_SCROLL_LOCK, ++i); // 16
++i; // 17
++i; // 18
++i; // 19
++i; // 20
SpecialKeysMap.put(KeyEvent.KEYCODE_F1, ++i); // 21
SpecialKeysMap.put(KeyEvent.KEYCODE_F2, ++i); // 22
SpecialKeysMap.put(KeyEvent.KEYCODE_F3, ++i); // 23
SpecialKeysMap.put(KeyEvent.KEYCODE_F4, ++i); // 24
SpecialKeysMap.put(KeyEvent.KEYCODE_F5, ++i); // 25
SpecialKeysMap.put(KeyEvent.KEYCODE_F6, ++i); // 26
SpecialKeysMap.put(KeyEvent.KEYCODE_F7, ++i); // 27
SpecialKeysMap.put(KeyEvent.KEYCODE_F8, ++i); // 28
SpecialKeysMap.put(KeyEvent.KEYCODE_F9, ++i); // 29
SpecialKeysMap.put(KeyEvent.KEYCODE_F10, ++i); // 30
SpecialKeysMap.put(KeyEvent.KEYCODE_F11, ++i); // 31
SpecialKeysMap.put(KeyEvent.KEYCODE_F12, ++i); // 21
}
}
public void setDeviceId(String id) {
deviceId = id;
}
public KeyListenerView(Context context, AttributeSet set) {
super(context, set);
setFocusable(true);
setFocusableInTouchMode(true);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
if (android.os.Build.VERSION.SDK_INT >= 11) {
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
}
return new KeyInputConnection(this, true);
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
public void sendChars(CharSequence chars) {
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
np.set("key", chars.toString());
sendKeyPressPacket(np);
}
private void sendKeyPressPacket(final NetworkPacket np) {
BackgroundService.RunCommand(getContext(), service -> {
Device device = service.getDevice(deviceId);
MousePadPlugin mousePadPlugin = device.getPlugin(MousePadPlugin.class);
if (mousePadPlugin == null) return;
mousePadPlugin.sendKeyboardPacket(np);
});
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//We don't want to swallow the back button press
return false;
}
// NOTE: Most keyboards, and specifically the Android default keyboard when
// entering non-ascii characters, will not trigger KeyEvent events as documented
// here: http://developer.android.com/reference/android/view/KeyEvent.html
//Log.e("KeyDown", "------------");
//Log.e("KeyDown", "keyChar:" + (int) event.getDisplayLabel());
//Log.e("KeyDown", "utfChar:" + (char)event.getUnicodeChar());
//Log.e("KeyDown", "intUtfChar:" + event.getUnicodeChar());
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
boolean modifier = false;
if (event.isAltPressed()) {
np.set("alt", true);
modifier = true;
}
if (Build.VERSION.SDK_INT >= 11) {
if (event.isCtrlPressed()) {
np.set("ctrl", true);
modifier = true;
}
}
if (event.isShiftPressed()) {
np.set("shift", true);
}
int specialKey = SpecialKeysMap.get(keyCode, -1);
if (specialKey != -1) {
np.set("specialKey", specialKey);
} else if (event.getDisplayLabel() != 0 && modifier) {
//Alt will change the utf symbol to non-ascii characters, we want the plain original letter
//Since getDisplayLabel will always have a value, we have to check for special keys before
char keyCharacter = event.getDisplayLabel();
np.set("key", new String(new char[]{keyCharacter}).toLowerCase());
} else {
//A normal key, but still not handled by the KeyInputConnection (happens with numbers)
np.set("key", new String(new char[]{(char) event.getUnicodeChar()}));
}
sendKeyPressPacket(np);
return true;
}
}
diff --git a/src/org/kde/kdeconnect/Plugins/PluginFactory.java b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
index deb66b35..f62bd1ba 100644
--- a/src/org/kde/kdeconnect/Plugins/PluginFactory.java
+++ b/src/org/kde/kdeconnect/Plugins/PluginFactory.java
@@ -1,232 +1,234 @@
/*
* 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.ContactsPlugin.ContactsPlugin;
import org.kde.kdeconnect.Plugins.FindMyPhonePlugin.FindMyPhonePlugin;
import org.kde.kdeconnect.Plugins.FindRemoteDevicePlugin.FindRemoteDevicePlugin;
import org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadPlugin;
import org.kde.kdeconnect.Plugins.MprisPlugin.MprisPlugin;
import org.kde.kdeconnect.Plugins.MprisReceiverPlugin.MprisReceiverPlugin;
import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationsPlugin;
import org.kde.kdeconnect.Plugins.PingPlugin.PingPlugin;
+import org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterPlugin;
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.SystemvolumePlugin.SystemvolumePlugin;
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[] supportedPacketTypes, String[] outgoingPacketTypes) {
this.displayName = displayName;
this.description = description;
this.icon = icon;
this.enabledByDefault = enabledByDefault;
this.hasSettings = hasSettings;
this.listenToUnpaired = listenToUnpaired;
HashSet incoming = new HashSet<>();
if (supportedPacketTypes != null) Collections.addAll(incoming, supportedPacketTypes);
this.supportedPacketTypes = Collections.unmodifiableSet(incoming);
HashSet outgoing = new HashSet<>();
if (outgoingPacketTypes != null) Collections.addAll(outgoing, outgoingPacketTypes);
this.outgoingPacketTypes = 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 getOutgoingPacketTypes() {
return outgoingPacketTypes;
}
public Set getSupportedPacketTypes() {
return supportedPacketTypes;
}
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 supportedPacketTypes;
private final Set outgoingPacketTypes;
}
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(PresenterPlugin.class);
PluginFactory.registerPlugin(SharePlugin.class);
PluginFactory.registerPlugin(TelepathyPlugin.class);
PluginFactory.registerPlugin(FindMyPhonePlugin.class);
PluginFactory.registerPlugin(RunCommandPlugin.class);
PluginFactory.registerPlugin(ContactsPlugin.class);
PluginFactory.registerPlugin(RemoteKeyboardPlugin.class);
PluginFactory.registerPlugin(SystemvolumePlugin.class);
//PluginFactory.registerPlugin(MprisReceiverPlugin.class);
PluginFactory.registerPlugin(FindRemoteDevicePlugin.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.getSupportedPacketTypes(), p.getOutgoingPacketTypes());
pluginInfoCache.put(pluginKey, info); //Cache it
return info;
} catch (Exception e) {
Log.e("PluginFactory", "getPluginInfo exception");
e.printStackTrace();
throw new RuntimeException(e);
}
}
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 extends Plugin> 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.getSupportedPacketTypes());
}
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.getOutgoingPacketTypes());
}
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.getSupportedPacketTypes())
&& Collections.disjoint(incoming, plugin.getOutgoingPacketTypes())) {
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/PresenterPlugin/PresenterActivity.java b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java
new file mode 100644
index 00000000..1fd0ec23
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterActivity.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2014 Ahmed I. Khalil
+ *
+ * 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.PresenterPlugin;
+
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import org.kde.kdeconnect.BackgroundService;
+import org.kde.kdeconnect.Device;
+import org.kde.kdeconnect.UserInterface.ThemeUtil;
+import org.kde.kdeconnect_tp.R;
+
+public class PresenterActivity extends AppCompatActivity {
+
+ private MediaSessionCompat mMediaSession;
+
+ PresenterPlugin plugin;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ThemeUtil.setUserPreferredTheme(this);
+
+ setContentView(R.layout.activity_presenter);
+
+ final String deviceId = getIntent().getStringExtra("deviceId");
+
+ BackgroundService.RunCommand(this, service -> {
+ Device device = service.getDevice(deviceId);
+
+ plugin = device.getPlugin(PresenterPlugin.class);
+ if (plugin == null) {
+ Log.e("PresenterActivity", "device has no presenter plugin!");
+ return;
+ }
+
+ runOnUiThread(() -> {
+
+ findViewById(R.id.next_button).setOnClickListener(v -> {
+ plugin.sendNext();
+ });
+
+ findViewById(R.id.previous_button).setOnClickListener(v -> {
+ plugin.sendPrevious();
+ });
+
+
+ });
+ });
+
+ }
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.menu_presenter, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch(item.getItemId()) {
+ case R.id.fullscreen:
+ plugin.sendFullscreen();
+ return true;
+ case R.id.exit_presentation:
+ plugin.sendEsc();
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ BackgroundService.addGuiInUseCounter(this);
+ if (mMediaSession != null) {
+ mMediaSession.setActive(true);
+ return;
+ }
+ createMediaSession(); //Mediasession will keep
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ BackgroundService.removeGuiInUseCounter(this);
+
+ if (mMediaSession != null) {
+ PowerManager pm = (PowerManager) this.getSystemService(Context.POWER_SERVICE);
+ boolean screenOn;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
+ screenOn = pm.isInteractive();
+ } else {
+ screenOn = pm.isScreenOn();
+ }
+ if (screenOn) {
+ mMediaSession.release();
+ } // else we are in the lockscreen, keep the mediasession
+ }
+ }
+
+ private void createMediaSession() {
+ mMediaSession = new MediaSessionCompat(this, "kdeconnect");
+
+ mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS |
+ MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
+ mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder()
+ .setState(PlaybackStateCompat.STATE_PLAYING, 0, 0)
+ .build());
+ mMediaSession.setPlaybackToRemote(getVolumeProvider());
+ mMediaSession.setActive(true);
+ }
+
+ private VolumeProviderCompat getVolumeProvider() {
+ final int VOLUME_UP = 1;
+ final int VOLUME_DOWN = -1;
+ return new VolumeProviderCompat(VolumeProviderCompat.VOLUME_CONTROL_RELATIVE, 1, 0) {
+ @Override
+ public void onAdjustVolume(int direction) {
+ if (direction == VOLUME_UP) {
+ plugin.sendNext();
+ }
+ else if (direction == VOLUME_DOWN) {
+ plugin.sendPrevious();
+ }
+ }
+ };
+ }
+
+}
+
diff --git a/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterPlugin.java b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterPlugin.java
new file mode 100644
index 00000000..1214c0cd
--- /dev/null
+++ b/src/org/kde/kdeconnect/Plugins/PresenterPlugin/PresenterPlugin.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2014 Ahmed I. Khalil
+ *
+ * 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.PresenterPlugin;
+
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.support.v4.content.ContextCompat;
+import android.view.KeyEvent;
+
+import org.kde.kdeconnect.NetworkPacket;
+import org.kde.kdeconnect.Plugins.Plugin;
+import org.kde.kdeconnect_tp.R;
+
+import static org.kde.kdeconnect.Plugins.MousePadPlugin.KeyListenerView.SpecialKeysMap;
+
+public class PresenterPlugin extends Plugin {
+
+ public final static String PACKET_TYPE_MOUSEPAD_REQUEST = "kdeconnect.mousepad.request";
+
+ @Override
+ public String getDisplayName() {
+ return context.getString(R.string.pref_plugin_presenter);
+ }
+
+ @Override
+ public String getDescription() {
+ return context.getString(R.string.pref_plugin_presenter_desc);
+ }
+
+ @Override
+ public Drawable getIcon() {
+ return ContextCompat.getDrawable(context, R.drawable.ic_presenter);
+ }
+
+ @Override
+ public boolean hasSettings() {
+ return false;
+ }
+
+ @Override
+ public boolean hasMainActivity() {
+ return true;
+ }
+
+ @Override
+ public void startMainActivity(Activity parentActivity) {
+ Intent intent = new Intent(parentActivity, PresenterActivity.class);
+ intent.putExtra("deviceId", device.getDeviceId());
+ parentActivity.startActivity(intent);
+ }
+
+ @Override
+ public String[] getSupportedPacketTypes() {
+ return new String[0];
+ }
+
+ @Override
+ public String[] getOutgoingPacketTypes() {
+ return new String[]{PACKET_TYPE_MOUSEPAD_REQUEST};
+ }
+
+ @Override
+ public String getActionName() {
+ return context.getString(R.string.pref_plugin_presenter);
+ }
+
+ public void sendNext() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
+ np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_PAGE_DOWN));
+ device.sendPacket(np);
+ }
+
+ public void sendPrevious() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
+ np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_PAGE_UP));
+ device.sendPacket(np);
+ }
+
+ public void sendFullscreen() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
+ np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_F5));
+ device.sendPacket(np);
+ }
+
+ public void sendEsc() {
+ NetworkPacket np = new NetworkPacket(PACKET_TYPE_MOUSEPAD_REQUEST);
+ np.set("specialKey", SpecialKeysMap.get(KeyEvent.KEYCODE_ESCAPE));
+ device.sendPacket(np);
+ }
+
+}