2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-31 06:05:12 +00:00

Merge branch 'pie'

# Conflicts:
#	res/values/strings.xml
#	src/org/kde/kdeconnect/UserInterface/MainActivity.java
This commit is contained in:
Albert Vaca
2018-10-25 21:06:18 +02:00
24 changed files with 571 additions and 209 deletions

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.kde.kdeconnect_tp"
android:versionCode="1841"
android:versionName="1.8.4">
android:versionCode="1902"
android:versionName="1.9 Beta">
<supports-screens
android:anyDensity="true"
@@ -17,11 +17,10 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<!--<uses-permission android:name="android.permission.BLUETOOTH" />-->
<!--<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />-->
<!-- <uses-permission android:name="android.permission.BLUETOOTH" /> -->
<!-- <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> -->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.BATTERY_STATS" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
@@ -31,19 +30,16 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<uses-permission android:name="android.permission.BIND_REMOTEVIEWS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="true"
android:icon="@drawable/icon"
android:label="KDE Connect"
android:supportsRtl="true"
android:theme="@style/KdeConnectTheme">
<service
android:name="org.kde.kdeconnect.BackgroundService"
android:enabled="true" />
<service
android:name="org.kde.kdeconnect.Plugins.RemoteKeyboardPlugin.RemoteKeyboardService"
android:label="KDE Connect Remote Keyboard"
@@ -51,6 +47,7 @@
<intent-filter>
<action android:name="android.view.InputMethod" />
</intent-filter>
<meta-data
android:name="android.view.im"
android:resource="@xml/remotekeyboardplugin_method" />
@@ -62,12 +59,12 @@
android:theme="@style/KdeConnectTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.UserInterface.SettingsActivity"
android:name="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
@@ -93,21 +90,20 @@
<activity
android:name="org.kde.kdeconnect.UserInterface.PluginSettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.SettingsActivity">
android:parentActivityName="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.SettingsActivity" />
android:value="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsActivity"
android:label="@string/device_menu_plugins"
android:parentActivityName="org.kde.kdeconnect.UserInterface.SettingsActivity">
android:parentActivityName="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.Plugins.SharePlugin.ShareSettingsActivity" />
</activity>
<receiver android:name="org.kde.kdeconnect.KdeConnectBroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
@@ -140,12 +136,13 @@
<activity
android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisActivity"
android:label="@string/remote_control"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity"
android:launchMode="singleTop">
android:launchMode="singleTop"
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<receiver android:name="org.kde.kdeconnect.Plugins.MprisPlugin.MprisMediaNotificationReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
@@ -159,44 +156,49 @@
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetDeviceSelector"
android:theme="@style/Theme.AppCompat.Light.Dialog"
android:launchMode="singleTask"
android:screenOrientation="user"
android:noHistory="true"
<activity
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetDeviceSelector"
android:excludeFromRecents="true"
android:label="@string/pref_plugin_runcommand"/>
android:label="@string/pref_plugin_runcommand"
android:launchMode="singleTask"
android:noHistory="true"
android:screenOrientation="user"
android:theme="@style/Theme.AppCompat.Light.Dialog" />
<service
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidgetDataProviderService"
android:exported="false" android:permission="android.permission.BIND_REMOTEVIEWS"/>
<receiver android:label="@string/pref_plugin_runcommand" android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidget">
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
<receiver
android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandWidget"
android:label="@string/pref_plugin_runcommand">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<intent-filter>
<action android:name="RUN_COMMAND_ACTION" />
</intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/remotecommandplugin_widget" />
</receiver>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/remotecommandplugin_widget" />
</receiver>
<activity android:name="org.kde.kdeconnect.Plugins.RunCommandPlugin.RunCommandUrlActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.nfc.action.NDEF_DISCOVERED"></action>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kdeconnect" android:host="runcommand"/>
<data
android:host="runcommand"
android:scheme="kdeconnect" />
</intent-filter>
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.MousePadActivity"
android:label="@string/remote_control"
@@ -205,7 +207,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
@@ -216,7 +217,6 @@
android:name="android.support.PARENT_ACTIVITY"
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
</activity>
<activity
android:name="org.kde.kdeconnect.Plugins.SharePlugin.ShareActivity"
android:label="KDE Connect">
@@ -274,7 +274,6 @@
android:value="org.kde.kdeconnect.UserInterface.PluginSettingsActivity" />
</activity>
</application>
</manifest>
</manifest>

View File

@@ -11,10 +11,10 @@ buildscript {
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
compileSdkVersion 28
defaultConfig {
minSdkVersion 9
targetSdkVersion 25
minSdkVersion 14
targetSdkVersion 28
//multiDexEnabled true
//testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
}
@@ -25,7 +25,7 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
sourceSets {
main {
manifest.srcFile 'AndroidManifest.xml'
java.srcDirs = ['src']
@@ -44,7 +44,6 @@ android {
pickFirst "META-INF/BCKEY.SF"
pickFirst "META-INF/BCKEY.DSA"
pickFirst "META-INF/INDEX.LIST"
pickFirst "META-INF/io.netty.versions.properties"
}
lintOptions {
abortOnError false
@@ -58,7 +57,7 @@ android {
release { //keep on 'release', set to 'all' when testing to make sure proguard is not deleting important stuff
minifyEnabled true
useProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
@@ -70,12 +69,13 @@ dependencies {
google()
}
implementation 'com.android.support:support-v4:25.4.0'
implementation 'com.android.support:appcompat-v7:25.4.0'
implementation 'com.android.support:design:25.4.0'
implementation 'com.android.support:support-media-compat:28.0.0'
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:preference-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps
implementation 'org.apache.sshd:sshd-core:0.8.0' //0.9 seems to fail on Android 6 and 1.+ requires java.nio.file, which doesn't exist in Android
implementation 'org.apache.sshd:sshd-core:0.14.0' //Newer version requires java.nio.file, which isn't available until Android 8+
implementation 'com.madgag.spongycastle:bcpkix-jdk15on:1.58.0.0' //For SSL certificate generation

5
proguard-rules.pro vendored
View File

@@ -37,3 +37,8 @@
-keep class org.apache.mina.** {*;}
-keep class org.kde.kdeconnect.** {*;}
-dontwarn org.mockito.**
-dontwarn sun.reflect.**
-dontwarn android.test.**
-dontwarn java.lang.management.**

View File

@@ -8,12 +8,6 @@
android:title="@string/refresh"
kdeconnect:showAsAction="never" />
<item
android:id="@+id/menu_rename"
android:orderInCategory="300"
android:title="@string/device_rename_title"
kdeconnect:showAsAction="never" />
<item
android:id="@+id/menu_custom_device_list"
android:orderInCategory="900"

View File

@@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="kde_connect">KDE Connect</string>
<string name="foreground_notification_no_devices">Not connected to any device</string>
<string name="foreground_notification_devices">Connected to: %s</string>
<string name="pref_plugin_telephony">Telephony notifier</string>
<string name="pref_plugin_telephony_desc">Send notifications for incoming calls</string>
<string name="pref_plugin_battery">Battery report</string>
@@ -151,7 +154,7 @@
<string name="device">Device</string>
<string name="pair_device">Pair device</string>
<string name="remote_control">Remote control</string>
<string name="settings">KDE Connect Settings</string>
<string name="settings">Settings</string>
<string name="mpris_play">Play</string>
<string name="mpris_pause">Pause</string>
<string name="mpris_previous">Previous</string>
@@ -260,7 +263,7 @@
<string name="presenter_fullscreen">Fullscreen</string>
<string name="presenter_exit">Exit presentation</string>
<string name="presenter_lock_tip">You can lock your device to use the volume keys as previous/next buttons</string>
<string name="add_command">Add a command</string>
<string name="addcommand_explanation">There are no commands registered</string>
<string name="addcommand_explanation2">You can add new commands in the KDE Connect System Settings</string>
@@ -269,6 +272,12 @@
<string name="pref_plugin_mprisreceiver_desc">Control your phones media players from another device</string>
<string name="dark_theme">Dark theme</string>
<string name="notification_channel_default">Other notifications</string>
<string name="notification_channel_persistent">Persistent indicator</string>
<string name="notification_channel_media_control">Media control</string>
<string name="notification_channel_filetransfer">File transfer</string>
<string name="mpris_stop">Stop the current player</string>
<string name="copy_url_to_clipboard">Copy URL to clipboard</string>
<string name="clipboard_toast">Copied to clipboard</string>
@@ -285,4 +294,14 @@
<string name="mute">Mute</string>
<string name="all">All</string>
<string name="devices">Devices</string>
<string name="settings_rename">Device name</string>
<string name="settings_dark_mode">Dark theme</string>
<string name="settings_more_settings_title">More settings</string>
<string name="settings_more_settings_text">Per-device settings can be found under \'Plugin settings\' from within a device.</string>
<string name="setting_persistent_notification">Show persistent notification</string>
<string name="setting_persistent_notification_oreo_description">Required by Android since Android 8.0</string>
<string name="setting_persistent_notification_pie_description">Since Android 9.0, this notification can only be minimized by long tapping on it</string>
</resources>

View File

@@ -20,6 +20,9 @@
package org.kde.kdeconnect;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
@@ -28,14 +31,19 @@ import android.content.SharedPreferences;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
import android.text.TextUtils;
import android.util.Log;
import org.kde.kdeconnect.Backends.BaseLink;
import org.kde.kdeconnect.Backends.BaseLinkProvider;
//import org.kde.kdeconnect.Backends.BluetoothBackend.BluetoothLinkProvider;
import org.kde.kdeconnect.Backends.LanBackend.LanLinkProvider;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect_tp.R;
import java.util.ArrayList;
import java.util.HashSet;
@@ -45,6 +53,7 @@ import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class BackgroundService extends Service {
public static final int FOREGROUND_NOTIFICATION_ID = 1;
private static BackgroundService instance;
@@ -128,6 +137,13 @@ public class BackgroundService extends Service {
for (DeviceListChangedCallback callback : deviceListChangedCallbacks.values()) {
callback.onDeviceListChanged();
}
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
//Update the foreground notification with the currently connected device list
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
}
private void loadRememberedDevicesFromSettings() {
@@ -259,6 +275,7 @@ public class BackgroundService extends Service {
Log.i("KDE/BackgroundService", "Service not started yet, initializing...");
initializeSecurityParameters();
NotificationHelper.initializeChannels(this);
loadRememberedDevicesFromSettings();
registerLinkProviders();
@@ -270,6 +287,54 @@ public class BackgroundService extends Service {
}
}
public void changePersistentNotificationVisibility(boolean visible) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (visible) {
nm.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
} else {
stopForeground(true);
Start(this);
}
}
private Notification createForegroundNotification() {
//Why is this needed: https://developer.android.com/guide/components/services#Foreground
Intent intent = new Intent(this, MainActivity.class);
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder notification = new NotificationCompat.Builder(this, NotificationHelper.Channels.PERSISTENT);
notification
.setSmallIcon(R.drawable.ic_notification)
.setOngoing(true)
.setContentIntent(pi)
.setPriority(NotificationCompat.PRIORITY_MIN) //MIN so it's not shown in the status bar before Oreo, on Oreo it will be bumped to LOW
.setShowWhen(false)
.setAutoCancel(false);
notification.setGroup("BackgroundService");
ArrayList<String> connectedDevices = new ArrayList<>();
for (Device device : getDevices().values()) {
if (device.isReachable() && device.isPaired()) {
connectedDevices.add(device.getName());
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
//Pre-oreo, the notification will have an empty title line without this
notification.setContentTitle("KDE Connect");
}
if (connectedDevices.isEmpty()) {
notification.setContentText(getString(R.string.foreground_notification_no_devices));
} else {
notification.setContentText(getString(R.string.foreground_notification_devices, TextUtils.join(", ", connectedDevices)));
}
return notification.build();
}
void initializeSecurityParameters() {
RsaHelper.initialiseRsaKeys(this);
SslHelper.initialiseCertificate(this);
@@ -277,6 +342,7 @@ public class BackgroundService extends Service {
@Override
public void onDestroy() {
stopForeground(true);
for (BaseLinkProvider a : linkProviders) {
a.onStop();
}
@@ -311,6 +377,10 @@ public class BackgroundService extends Service {
} finally {
mutex.unlock();
}
if (NotificationHelper.isPersistentNotificationEnabled(this)) {
startForeground(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
return Service.START_STICKY;
}

View File

@@ -404,7 +404,9 @@ public class Device implements BaseLink.PacketReceiver {
Resources res = getContext().getResources();
Notification noti = new NotificationCompat.Builder(getContext())
final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(getContext(), NotificationHelper.Channels.DEFAULT)
.setContentTitle(res.getString(R.string.pairing_request_from, getName()))
.setContentText(res.getString(R.string.tap_to_answer))
.setContentIntent(pendingIntent)
@@ -416,7 +418,6 @@ public class Device implements BaseLink.PacketReceiver {
.setDefaults(Notification.DEFAULT_ALL)
.build();
final NotificationManager notificationManager = (NotificationManager) getContext().getSystemService(Context.NOTIFICATION_SERVICE);
NotificationHelper.notifyCompat(notificationManager, notificationId, noti);
BackgroundService.addGuiInUseCounter(context);

View File

@@ -1,10 +1,24 @@
package org.kde.kdeconnect.Helpers;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import org.kde.kdeconnect_tp.R;
public class NotificationHelper {
public static class Channels {
public final static String PERSISTENT = "persistent";
public final static String DEFAULT = "default";
public final static String MEDIA_CONTROL = "media_control";
public final static String FILETRANSFER = "filetransfer";
}
public static void notifyCompat(NotificationManager notificationManager, int notificationId, Notification notification) {
try {
notificationManager.notify(notificationId, notification);
@@ -22,4 +36,57 @@ public class NotificationHelper {
//https://android.googlesource.com/platform/frameworks/base/+/android-4.2.1_r1.2%5E%5E!/
}
}
public static void initializeChannels(Context context) {
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
return;
}
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel persistentChannel = new NotificationChannel(
Channels.PERSISTENT,
context.getString(R.string.notification_channel_persistent),
NotificationManager.IMPORTANCE_MIN);
manager.createNotificationChannel(persistentChannel);
manager.createNotificationChannel(new NotificationChannel(
Channels.DEFAULT,
context.getString(R.string.notification_channel_default),
NotificationManager.IMPORTANCE_DEFAULT)
);
manager.createNotificationChannel(new NotificationChannel(
Channels.MEDIA_CONTROL,
context.getString(R.string.notification_channel_media_control),
NotificationManager.IMPORTANCE_LOW)
);
NotificationChannel fileTransfer = new NotificationChannel(
Channels.FILETRANSFER,
context.getString(R.string.notification_channel_filetransfer),
NotificationManager.IMPORTANCE_LOW);
fileTransfer.enableVibration(false);
manager.createNotificationChannel(fileTransfer);
}
public static void setPersistentNotificationEnabled(Context context, boolean enabled) {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
prefs.edit().putBoolean("persistentNotification", enabled).apply();
}
public static boolean isPersistentNotificationEnabled(Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
return true;
}
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
return prefs.getBoolean("persistentNotification", false);
}
}

View File

@@ -30,13 +30,15 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import android.support.v4.media.app.NotificationCompat.MediaStyle;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect_tp.R;
import java.util.HashSet;
@@ -304,8 +306,10 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
iOpenActivity.putExtra("player", notificationPlayer.getPlayer());
PendingIntent piOpenActivity = PendingIntent.getActivity(service, 0, iOpenActivity, PendingIntent.FLAG_UPDATE_CURRENT);
//Create the notification
final NotificationCompat.Builder notification = new NotificationCompat.Builder(service);
final NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationCompat.Builder notification = new NotificationCompat.Builder(context, NotificationHelper.Channels.MEDIA_CONTROL);
notification
.setAutoCancel(false)
.setContentIntent(piOpenActivity)
@@ -377,7 +381,7 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
}
//Use the MediaStyle notification, so it feels like other media players. That also allows adding actions
NotificationCompat.MediaStyle mediaStyle = new NotificationCompat.MediaStyle();
MediaStyle mediaStyle = new MediaStyle();
if (numActions == 1) {
mediaStyle.setShowActionsInCompactView(0);
} else if (numActions == 2) {
@@ -387,6 +391,7 @@ public class MprisMediaSession implements SharedPreferences.OnSharedPreferenceCh
}
mediaStyle.setMediaSession(mediaSession.getSessionToken());
notification.setStyle(mediaStyle);
notification.setGroup("MprisMediaSession");
//Display the notification
mediaSession.setActive(true);

View File

@@ -45,8 +45,8 @@ import android.util.Log;
import org.kde.kdeconnect.Helpers.AppsHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
import org.kde.kdeconnect.UserInterface.MainActivity;
import org.kde.kdeconnect.UserInterface.SettingsActivity;
import org.kde.kdeconnect_tp.R;
import java.io.ByteArrayOutputStream;
@@ -84,7 +84,7 @@ public class NotificationsPlugin extends Plugin implements NotificationReceiver.
}
@Override
public void startPreferencesActivity(final SettingsActivity parentActivity) {
public void startPreferencesActivity(final DeviceSettingsActivity parentActivity) {
if (hasPermission()) {
Intent intent = new Intent(parentActivity, NotificationFilterActivity.class);
parentActivity.startActivity(intent);

View File

@@ -79,7 +79,9 @@ public class PingPlugin extends Plugin {
id = 42; //A unique id to create only one notification
}
Notification noti = new NotificationCompat.Builder(context)
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(device.getName())
.setContentText(message)
.setContentIntent(resultPendingIntent)
@@ -89,7 +91,6 @@ public class PingPlugin extends Plugin {
.setDefaults(Notification.DEFAULT_ALL)
.build();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationHelper.notifyCompat(notificationManager, id, noti);
return true;

View File

@@ -34,8 +34,8 @@ import android.widget.Button;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
import org.kde.kdeconnect.UserInterface.PluginSettingsActivity;
import org.kde.kdeconnect.UserInterface.SettingsActivity;
import org.kde.kdeconnect_tp.R;
public abstract class Plugin {
@@ -126,9 +126,9 @@ public abstract class Plugin {
* If hasSettings returns true, this will be called when the user
* wants to access this plugin preferences and should launch some
* kind of interface. The default implementation will launch a
* SettingsActivity with content from "yourplugin"_preferences.xml.
* PluginSettingsActivity with content from "yourplugin"_preferences.xml.
*/
public void startPreferencesActivity(SettingsActivity parentActivity) {
public void startPreferencesActivity(DeviceSettingsActivity parentActivity) {
Intent intent = new Intent(parentActivity, PluginSettingsActivity.class);
intent.putExtra("plugin_display_name", getDisplayName());
intent.putExtra("plugin_key", getPluginKey());

View File

@@ -105,7 +105,10 @@ public class ReceiveNotificationsPlugin extends Plugin {
}
}
}
Notification noti = new NotificationCompat.Builder(context)
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(np.getString("appName"))
.setContentText(np.getString("ticker"))
.setContentIntent(resultPendingIntent)
@@ -117,7 +120,6 @@ public class ReceiveNotificationsPlugin extends Plugin {
.setDefaults(Notification.DEFAULT_ALL)
.build();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationHelper.notifyCompat(notificationManager, "kdeconnectId:" + np.getString("id", "0"), np.getInt("id", 0), noti);
}

View File

@@ -51,9 +51,14 @@ public class SftpPlugin extends Plugin {
@Override
public boolean onCreate() {
server.init(context, device);
permissionExplanation = R.string.sftp_permission_explanation;
return true;
try {
server.init(context, device);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
@Override

View File

@@ -27,24 +27,25 @@ import android.util.Log;
import org.apache.sshd.SshServer;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.Session;
import org.apache.sshd.common.file.FileSystemFactory;
import org.apache.sshd.common.file.FileSystemView;
import org.apache.sshd.common.file.SshFile;
import org.apache.sshd.common.file.nativefs.NativeFileSystemView;
import org.apache.sshd.common.file.nativefs.NativeSshFile;
import org.apache.sshd.common.keyprovider.AbstractKeyPairProvider;
import org.apache.sshd.common.util.SecurityUtils;
import org.apache.sshd.server.Command;
import org.apache.sshd.server.FileSystemFactory;
import org.apache.sshd.server.FileSystemView;
import org.apache.sshd.server.PasswordAuthenticator;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.SshFile;
import org.apache.sshd.server.command.ScpCommandFactory;
import org.apache.sshd.server.filesystem.NativeFileSystemView;
import org.apache.sshd.server.filesystem.NativeSshFile;
import org.apache.sshd.server.kex.DHG1;
import org.apache.sshd.server.kex.DHG14;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
import org.apache.sshd.server.session.ServerSession;
import org.apache.sshd.server.sftp.SftpSubsystem;
import org.kde.kdeconnect.Device;
import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.Helpers.RandomHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.RsaHelper;
import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper;
import java.io.File;
@@ -56,6 +57,8 @@ import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.util.Arrays;
@@ -81,13 +84,23 @@ class SimpleSftpServer {
private final SshServer sshd = SshServer.setUpDefaultServer();
public void init(Context context, Device device) {
public void init(Context context, Device device) throws Exception {
sshd.setKeyExchangeFactories(Arrays.asList(
new DHG14.Factory(),
new DHG1.Factory()));
sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(context.getFilesDir() + "/sftpd.ser"));
//Reuse this device keys for the ssh connection as well
final KeyPair keyPair;
PrivateKey privateKey = RsaHelper.getPrivateKey(context);
PublicKey publicKey = RsaHelper.getPublicKey(context);
keyPair = new KeyPair(publicKey, privateKey);
sshd.setKeyPairProvider(new AbstractKeyPairProvider() {
@Override
public Iterable<KeyPair> loadKeys() {
return Collections.singletonList(keyPair);
}
});
sshd.setFileSystemFactory(new AndroidFileSystemFactory(context));
sshd.setCommandFactory(new ScpCommandFactory());
@@ -204,7 +217,7 @@ class SimpleSftpServer {
@Override
protected SshFile getFile(final String dir, final String file) {
File fileObj = new File(dir, file);
return new AndroidSshFile(fileObj, userName, context);
return new AndroidSshFile(this, fileObj, userName, context);
}
}
@@ -213,8 +226,8 @@ class SimpleSftpServer {
final private Context context;
final private File file;
public AndroidSshFile(final File file, final String userName, Context context) {
super(file.getAbsolutePath(), file, userName);
public AndroidSshFile(final AndroidFileSystemView view, final File file, final String userName, Context context) {
super(view, file.getAbsolutePath(), file, userName);
this.context = context;
this.file = file;
}

View File

@@ -39,8 +39,9 @@ class NotificationUpdateCallback extends Device.SendPacketStatusCallback {
} else {
title = res.getString(R.string.outgoing_file_title, device.getName());
}
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(context)
notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setSmallIcon(android.R.drawable.stat_sys_upload)
.setAutoCancel(true)
.setProgress(100, 0, false)

View File

@@ -55,7 +55,7 @@ public class ShareNotification {
this.filename = filename;
notificationId = (int) System.currentTimeMillis();
notificationManager = (NotificationManager) device.getContext().getSystemService(Context.NOTIFICATION_SERVICE);
builder = new NotificationCompat.Builder(device.getContext())
builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.FILETRANSFER)
.setContentTitle(device.getContext().getResources().getString(R.string.incoming_file_title, device.getName()))
.setContentText(device.getContext().getResources().getString(R.string.incoming_file_text, filename))
.setTicker(device.getContext().getResources().getString(R.string.incoming_file_title, device.getName()))
@@ -80,7 +80,7 @@ public class ShareNotification {
public void setFinished(boolean success) {
String message = success ? device.getContext().getResources().getString(R.string.received_file_title, device.getName()) : device.getContext().getResources().getString(R.string.received_file_fail_title, device.getName());
builder = new NotificationCompat.Builder(device.getContext());
builder = new NotificationCompat.Builder(device.getContext(), NotificationHelper.Channels.DEFAULT);
builder.setContentTitle(message)
.setTicker(message)
.setSmallIcon(android.R.drawable.stat_sys_download_done)

View File

@@ -50,7 +50,7 @@ import org.kde.kdeconnect.Helpers.MediaStoreHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect.NetworkPacket;
import org.kde.kdeconnect.Plugins.Plugin;
import org.kde.kdeconnect.UserInterface.SettingsActivity;
import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity;
import org.kde.kdeconnect_tp.R;
import java.io.File;
@@ -159,7 +159,9 @@ public class SharePlugin extends Plugin {
PendingIntent.FLAG_UPDATE_CURRENT
);
Notification noti = new NotificationCompat.Builder(context)
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
Notification noti = new NotificationCompat.Builder(context, NotificationHelper.Channels.DEFAULT)
.setContentTitle(res.getString(R.string.received_url_title, device.getName()))
.setContentText(res.getString(R.string.received_url_text, url))
.setContentIntent(resultPendingIntent)
@@ -169,7 +171,6 @@ public class SharePlugin extends Plugin {
.setDefaults(Notification.DEFAULT_ALL)
.build();
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationHelper.notifyCompat(notificationManager, (int) System.currentTimeMillis(), noti);
}
}
@@ -280,7 +281,7 @@ public class SharePlugin extends Plugin {
}
@Override
public void startPreferencesActivity(SettingsActivity parentActivity) {
public void startPreferencesActivity(DeviceSettingsActivity parentActivity) {
Intent intent = new Intent(parentActivity, ShareSettingsActivity.class);
intent.putExtra("plugin_display_name", getDisplayName());
intent.putExtra("plugin_key", getPluginKey());

View File

@@ -182,6 +182,7 @@ public class DeviceFragment extends Fragment {
return;
}
//Plugins button list
final Collection<Plugin> plugins = device.getLoadedPlugins().values();
for (final Plugin p : plugins) {
@@ -195,7 +196,7 @@ public class DeviceFragment extends Fragment {
}
menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(menuItem -> {
Intent intent = new Intent(mActivity, SettingsActivity.class);
Intent intent = new Intent(mActivity, DeviceSettingsActivity.class);
intent.putExtra("deviceId", mDeviceId);
startActivity(intent);
return true;

View File

@@ -29,7 +29,7 @@ import org.kde.kdeconnect.Device;
import java.util.List;
public class SettingsActivity extends AppCompatPreferenceActivity {
public class DeviceSettingsActivity extends AppCompatPreferenceActivity {
static private String deviceId; //Static because if we get here by using the back button in the action bar, the extra deviceId will not be set.
@@ -47,12 +47,12 @@ public class SettingsActivity extends AppCompatPreferenceActivity {
BackgroundService.RunCommand(getApplicationContext(), service -> {
final Device device = service.getDevice(deviceId);
if (device == null) {
SettingsActivity.this.runOnUiThread(SettingsActivity.this::finish);
DeviceSettingsActivity.this.runOnUiThread(DeviceSettingsActivity.this::finish);
return;
}
List<String> plugins = device.getSupportedPlugins();
for (final String pluginKey : plugins) {
PluginPreference pref = new PluginPreference(SettingsActivity.this, pluginKey, device);
PluginPreference pref = new PluginPreference(DeviceSettingsActivity.this, pluginKey, device);
preferenceScreen.addPreference(pref);
}
});

View File

@@ -6,6 +6,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
@@ -24,10 +25,12 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Device;
@@ -36,10 +39,19 @@ import org.kde.kdeconnect_tp.R;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes;
public class MainActivity extends AppCompatActivity {
private static final String STATE_SELECTED_DEVICE = "selected_device";
private static final int MENU_ENTRY_ADD_DEVICE = 1; //0 means no-selection
private static final int MENU_ENTRY_SETTINGS = 2;
private static final int MENU_ENTRY_DEVICE_FIRST_ID = 1000; //All subsequent ids are devices in the menu
private static final int MENU_ENTRY_DEVICE_UNKNOWN = 9999; //It's still a device, but we don't know which one yet
private static final String STATE_SELECTED_MENU_ENTRY = "selected_entry"; //Saved only in onSaveInstanceState
private static final String STATE_SELECTED_DEVICE = "selected_device"; //Saved persistently in preferences
public static final int RESULT_NEEDS_RELOAD = Activity.RESULT_FIRST_USER;
@@ -51,10 +63,12 @@ public class MainActivity extends AppCompatActivity {
private DrawerLayout mDrawerLayout;
private String mCurrentDevice;
private int mCurrentMenuEntry;
private SharedPreferences preferences;
private final HashMap<MenuItem, String> mMapMenuToDeviceId = new HashMap<>();
private String pairRequestStatus;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -64,11 +78,11 @@ public class MainActivity extends AppCompatActivity {
ThemeUtil.setUserPreferredTheme(this);
setContentView(R.layout.activity_main);
mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
mNavigationView = (NavigationView) findViewById(R.id.navigation_drawer);
mDrawerLayout = findViewById(R.id.drawer_layout);
mNavigationView = findViewById(R.id.navigation_drawer);
View mDrawerHeader = mNavigationView.getHeaderView(0);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
@@ -87,100 +101,111 @@ public class MainActivity extends AppCompatActivity {
mDrawerToggle.syncState();
String deviceName = DeviceHelper.getDeviceName(this);
TextView nameView = (TextView) mDrawerHeader.findViewById(R.id.device_name);
TextView nameView = mDrawerHeader.findViewById(R.id.device_name);
nameView.setText(deviceName);
View.OnClickListener renameListener = v -> renameDevice();
View.OnClickListener renameListener = v -> openRenameDeviceDialog();
mDrawerHeader.findViewById(R.id.kdeconnect_label).setOnClickListener(renameListener);
mDrawerHeader.findViewById(R.id.device_name).setOnClickListener(renameListener);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
addDarkModeSwitch((ViewGroup) mDrawerHeader);
}
preferences = getSharedPreferences("stored_menu_selection", Context.MODE_PRIVATE);
mNavigationView.setNavigationItemSelectedListener(menuItem -> {
String deviceId = mMapMenuToDeviceId.get(menuItem);
onDeviceSelected(deviceId);
mCurrentMenuEntry = menuItem.getItemId();
switch (mCurrentMenuEntry) {
case MENU_ENTRY_ADD_DEVICE:
mCurrentDevice = null;
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
setContentFragment(new PairingFragment());
break;
case MENU_ENTRY_SETTINGS:
mCurrentDevice = null;
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
setContentFragment(new SettingsFragment());
break;
default:
assert(mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID);
String deviceId = mMapMenuToDeviceId.get(menuItem);
onDeviceSelected(deviceId);
break;
}
mDrawerLayout.closeDrawer(mNavigationView);
return true;
});
preferences = getSharedPreferences(STATE_SELECTED_DEVICE, Context.MODE_PRIVATE);
// Decide which menu entry should be selected at start
String savedDevice;
String pairStatus = "";
int savedMenuEntry;
if (getIntent().hasExtra("forceOverview")) {
Log.i("MainActivity", "Requested to start main overview");
savedDevice = null;
savedMenuEntry = MENU_ENTRY_ADD_DEVICE;
} else if (getIntent().hasExtra("deviceId")) {
Log.i("MainActivity", "Loading selected device from parameter");
savedDevice = getIntent().getStringExtra("deviceId");
if (getIntent().hasExtra(PAIR_REQUEST_STATUS)) {
pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS);
savedMenuEntry = MENU_ENTRY_DEVICE_UNKNOWN;
// If pairStatus is not empty, then the user has accepted/reject the pairing from the notification
String pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS);
if (pairStatus != null) {
Log.i("MainActivity", "pair status is " + pairStatus);
onPairResultFromNotification(savedDevice, pairStatus);
}
} else if (savedInstanceState != null) {
Log.i("MainActivity", "Loading selected device from saved activity state");
savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE);
savedMenuEntry = savedInstanceState.getInt(STATE_SELECTED_MENU_ENTRY, MENU_ENTRY_ADD_DEVICE);
} else {
Log.i("MainActivity", "Loading selected device from persistent storage");
savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null);
savedMenuEntry = (savedDevice != null)? MENU_ENTRY_DEVICE_UNKNOWN : MENU_ENTRY_ADD_DEVICE;
}
//if pairStatus is not empty, then the decision has been made...
if (!pairStatus.equals("")) {
Log.i("MainActivity", "pair status is " + pairStatus);
onNewDeviceSelected(savedDevice, pairStatus);
}
onDeviceSelected(savedDevice);
}
/**
* Adds a {@link SwitchCompat} to the bottom of the navigation header for
* toggling dark mode on and off. Call from {@link #onCreate(Bundle)}.
* <p>
* Only supports android ICS and higher because {@link SwitchCompat}
* requires that.
* </p>
*
* @param drawerHeader the layout which should contain the switch
*/
@RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
private void addDarkModeSwitch(ViewGroup drawerHeader) {
getLayoutInflater().inflate(R.layout.nav_dark_mode_switch, drawerHeader);
SwitchCompat darkThemeSwitch = (SwitchCompat) drawerHeader.findViewById(R.id.dark_theme);
darkThemeSwitch.setChecked(ThemeUtil.shouldUseDarkTheme(this));
darkThemeSwitch.setOnCheckedChangeListener((darkThemeSwitch1, isChecked) -> {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
boolean isDarkAlready = prefs.getBoolean("darkTheme", false);
if (isDarkAlready != isChecked) {
prefs.edit().putBoolean("darkTheme", isChecked).apply();
MainActivity.this.recreate();
// Activate the chosen fragment and select the entry in the menu
if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) {
onDeviceSelected(savedDevice);
} else {
mCurrentMenuEntry = savedMenuEntry;
mNavigationView.setCheckedItem(savedMenuEntry);
if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS) {
setContentFragment(new SettingsFragment());
} else {
setContentFragment(new PairingFragment());
}
});
}
}
//like onNewDeviceSelected but assumes that the new device is simply requesting to be paired
//and can't be null
private void onNewDeviceSelected(String deviceId, String pairStatus) {
private void onPairResultFromNotification(String deviceId, String pairStatus) {
assert(deviceId != null);
mCurrentDevice = deviceId;
preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply();
preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply();
for (HashMap.Entry<MenuItem, String> entry : mMapMenuToDeviceId.entrySet()) {
boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe
entry.getKey().setChecked(selected);
}
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice);
mNavigationView.setCheckedItem(mCurrentMenuEntry);
if (pairStatus.equals(PAIRING_ACCEPTED)) {
DeviceFragment.acceptPairing(deviceId, this);
DeviceFragment.acceptPairing(mCurrentDevice, this);
} else {
DeviceFragment.rejectPairing(deviceId, this);
DeviceFragment.rejectPairing(mCurrentDevice, this);
}
}
private int deviceIdToMenuEntryId(String deviceId) {
for (HashMap.Entry<MenuItem, String> entry : mMapMenuToDeviceId.entrySet()) {
if (TextUtils.equals(entry.getValue(), deviceId)) { //null-safe
return entry.getKey().getItemId();
}
}
return MENU_ENTRY_DEVICE_UNKNOWN;
}
@Override
public void onBackPressed() {
if (mDrawerLayout.isDrawerOpen(mNavigationView)) {
@@ -200,43 +225,50 @@ public class MainActivity extends AppCompatActivity {
}
}
private void updateComputerList() {
//Log.e("MainActivity", "UpdateComputerList");
private void updateDeviceList() {
BackgroundService.RunCommand(MainActivity.this, service -> {
Menu menu = mNavigationView.getMenu();
menu.clear();
mMapMenuToDeviceId.clear();
int id = 0;
SubMenu devicesMenu = menu.addSubMenu(R.string.devices);
int id = MENU_ENTRY_DEVICE_FIRST_ID;
Collection<Device> devices = service.getDevices().values();
for (Device device : devices) {
if (device.isReachable() && device.isPaired()) {
MenuItem item = menu.add(0, id++, 0, device.getName());
MenuItem item = devicesMenu.add(Menu.FIRST, id++, 1, 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);
MenuItem addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device);
addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline);
addDeviceItem.setCheckable(true);
MenuItem settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings);
settingsItem.setIcon(R.drawable.ic_action_settings);
settingsItem.setCheckable(true);
//Ids might have changed
if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) {
mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice);
}
mNavigationView.setCheckedItem(mCurrentMenuEntry);
});
}
@Override
protected void onStart() {
super.onStart();
BackgroundService.addGuiInUseCounter(this, true);
BackgroundService.RunCommand(this, service -> service.addDeviceListChangedCallback("MainActivity", this::updateComputerList));
updateComputerList();
BackgroundService.RunCommand(this, service -> service.addDeviceListChangedCallback("MainActivity", this::updateDeviceList));
updateDeviceList();
}
@Override
@@ -246,27 +278,38 @@ public class MainActivity extends AppCompatActivity {
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...)
private static void uncheckAllMenuItems(Menu menu) {
int size = menu.size();
for (int i = 0; i < size; i++) {
MenuItem item = menu.getItem(i);
if(item.hasSubMenu()) {
uncheckAllMenuItems(item.getSubMenu());
} else {
item.setChecked(false);
}
}
}
public void onDeviceSelected(String deviceId, boolean fromDeviceList) {
mCurrentDevice = deviceId;
preferences.edit().putString(STATE_SELECTED_DEVICE, deviceId).apply();
preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply();
for (HashMap.Entry<MenuItem, String> entry : mMapMenuToDeviceId.entrySet()) {
boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe
entry.getKey().setChecked(selected);
}
Fragment fragment;
if (deviceId == null) {
fragment = new PairingFragment();
if (mCurrentDevice != null) {
mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId);
if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) {
uncheckAllMenuItems(mNavigationView.getMenu());
} else {
mNavigationView.setCheckedItem(mCurrentMenuEntry);
}
setContentFragment(new DeviceFragment(deviceId, fromDeviceList));
} else {
fragment = new DeviceFragment(deviceId, fromDeviceList);
mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE;
mNavigationView.setCheckedItem(mCurrentMenuEntry);
setContentFragment(new PairingFragment());
}
}
private void setContentFragment(Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.container, fragment)
@@ -281,13 +324,7 @@ public class MainActivity extends AppCompatActivity {
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);
outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry);
}
@Override
@@ -317,28 +354,45 @@ public class MainActivity extends AppCompatActivity {
}
}
public void renameDevice() {
final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name);
final EditText deviceNameEdit = new EditText(MainActivity.this);
String deviceName = DeviceHelper.getDeviceName(MainActivity.this);
interface NameChangeCallback {
void onNameChanged(String newName);
}
private Set<NameChangeCallback> nameChangeSubscribers = new HashSet<>();
public void addNameChangeCallback(NameChangeCallback cb) {
nameChangeSubscribers.add(cb);
}
public void removeNameChangeCallback(NameChangeCallback cb) {
nameChangeSubscribers.remove(cb);
}
public void openRenameDeviceDialog() {
final EditText deviceNameEdit = new EditText(this);
String deviceName = DeviceHelper.getDeviceName(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(MainActivity.this)
float dpi = this.getResources().getDisplayMetrics().density;
deviceNameEdit.setPadding( ((int) (18 * dpi)), ((int) (16 * dpi)), ((int) (18 * dpi)), ((int) (12 * dpi)) );
new AlertDialog.Builder(this)
.setView(deviceNameEdit)
.setPositiveButton(R.string.device_rename_confirm, (dialog, which) -> {
String deviceName1 = deviceNameEdit.getText().toString();
DeviceHelper.setDeviceName(MainActivity.this, deviceName1);
nameView.setText(deviceName1);
BackgroundService.RunCommand(MainActivity.this, BackgroundService::onNetworkChange);
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {
String newDeviceName = deviceNameEdit.getText().toString();
DeviceHelper.setDeviceName(this, newDeviceName);
this.updateDeviceNameFromMenu(newDeviceName);
BackgroundService.RunCommand(this, BackgroundService::onNetworkChange);
for (NameChangeCallback callback : nameChangeSubscribers) {
callback.onNameChanged(newDeviceName);
}
})
.setNegativeButton(R.string.cancel, (dialog, which) -> { })
.setTitle(R.string.device_rename_title)
.show();
}
public void updateDeviceNameFromMenu(String newDeviceName) {
final TextView nameView = mNavigationView.findViewById(R.id.device_name);
nameView.setText(newDeviceName);
}
}

View File

@@ -237,9 +237,6 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb
case R.id.menu_refresh:
updateComputerListAction();
break;
case R.id.menu_rename:
mActivity.renameDevice();
break;
case R.id.menu_custom_device_list:
startActivity(new Intent(mActivity, CustomDevicesActivity.class));
break;

View File

@@ -14,7 +14,7 @@ public class PluginPreference extends CheckBoxPreference {
final String pluginKey;
final View.OnClickListener listener;
public PluginPreference(final SettingsActivity activity, final String pluginKey, final Device device) {
public PluginPreference(final DeviceSettingsActivity activity, final String pluginKey, final Device device) {
super(activity);
setLayoutResource(R.layout.preference_with_button);

View File

@@ -0,0 +1,127 @@
package org.kde.kdeconnect.UserInterface;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v14.preference.SwitchPreference;
import android.support.v7.preference.CheckBoxPreference;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceFragmentCompat;
import android.support.v7.preference.PreferenceScreen;
import android.support.v7.preference.TwoStatePreference;
import org.kde.kdeconnect.BackgroundService;
import org.kde.kdeconnect.Helpers.DeviceHelper;
import org.kde.kdeconnect.Helpers.NotificationHelper;
import org.kde.kdeconnect_tp.R;
public class SettingsFragment extends PreferenceFragmentCompat implements MainActivity.NameChangeCallback {
MainActivity mainActivity;
private Preference renameDevice;
@Override
public void onDestroy() {
mainActivity.removeNameChangeCallback(this);
super.onDestroy();
}
TwoStatePreference createTwoStatePreferenceCompat(Context context) {
//SwitchPreference doesn't work on Android 6
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
return new SwitchPreference(context);
} else {
return new CheckBoxPreference(context);
}
}
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
mainActivity = (MainActivity)getActivity();
Context context = mainActivity;
PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
// Rename device
mainActivity.addNameChangeCallback(this);
renameDevice = new Preference(context);
renameDevice.setPersistent(false);
renameDevice.setSelectable(true);
renameDevice.setOnPreferenceClickListener(preference -> {
mainActivity.openRenameDeviceDialog();
return true;
});
String deviceName = DeviceHelper.getDeviceName(context);
renameDevice.setTitle(R.string.settings_rename);
renameDevice.setSummary(deviceName);
screen.addPreference(renameDevice);
//TODO: Trusted wifi networks settings should go here
// Dark mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
final TwoStatePreference darkThemeSwitch = createTwoStatePreferenceCompat(context);
darkThemeSwitch.setPersistent(false);
darkThemeSwitch.setChecked(ThemeUtil.shouldUseDarkTheme(context));
darkThemeSwitch.setTitle(R.string.settings_dark_mode);
darkThemeSwitch.setOnPreferenceChangeListener((preference, newValue) -> {
boolean isChecked = (Boolean)newValue;
boolean isDarkAlready = prefs.getBoolean("darkTheme", false);
if (isDarkAlready != isChecked) {
prefs.edit().putBoolean("darkTheme", isChecked).apply();
if (mainActivity != null) {
mainActivity.recreate();
}
}
return true;
});
screen.addPreference(darkThemeSwitch);
}
// Persistent notification toggle
final TwoStatePreference notificationSwitch = createTwoStatePreferenceCompat(context);
notificationSwitch.setPersistent(false);
notificationSwitch.setChecked(NotificationHelper.isPersistentNotificationEnabled(context));
notificationSwitch.setTitle(R.string.setting_persistent_notification);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
notificationSwitch.setSummary(R.string.setting_persistent_notification_pie_description);
notificationSwitch.setEnabled(false);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
notificationSwitch.setSummary(R.string.setting_persistent_notification_oreo_description);
notificationSwitch.setEnabled(false);
}
notificationSwitch.setOnPreferenceChangeListener((preference, newValue) -> {
final boolean isChecked = (Boolean)newValue;
NotificationHelper.setPersistentNotificationEnabled(context, isChecked);
BackgroundService.RunCommand(context, service -> {
service.changePersistentNotificationVisibility(isChecked);
});
return true;
});
screen.addPreference(notificationSwitch);
// More settings text
Preference moreSettingsText = new Preference(context);
moreSettingsText.setPersistent(false);
moreSettingsText.setSelectable(false);
moreSettingsText.setTitle(R.string.settings_more_settings_title);
moreSettingsText.setSummary(R.string.settings_more_settings_text);
screen.addPreference(moreSettingsText);
setPreferenceScreen(screen);
}
@Override
public void onNameChanged(String newName) {
renameDevice.setSummary(newName);
}
}