diff --git a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java index e1087c53..16d4d05a 100644 --- a/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/MprisReceiverPlugin/MprisReceiverPlugin.java @@ -1,231 +1,231 @@ /* * Copyright 2018 Nicolas Fella * * 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.MprisReceiverPlugin; import android.app.Activity; -import android.app.AlertDialog; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.media.session.MediaController; import android.media.session.MediaSessionManager; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.util.Log; import org.kde.kdeconnect.Helpers.AppsHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.NotificationsPlugin.NotificationReceiver; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.MainActivity; import org.kde.kdeconnect_tp.R; import java.util.HashMap; import java.util.List; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class MprisReceiverPlugin extends Plugin implements MediaSessionManager.OnActiveSessionsChangedListener { private final static String PACKET_TYPE_MPRIS = "kdeconnect.mpris"; private final static String PACKET_TYPE_MPRIS_REQUEST = "kdeconnect.mpris.request"; private static final String TAG = "MprisReceiver"; private HashMap players; @Override public boolean onCreate() { if (!hasPermission()) return false; players = new HashMap<>(); try { MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); if (null == manager) return false; manager.addOnActiveSessionsChangedListener(MprisReceiverPlugin.this, new ComponentName(context, NotificationReceiver.class), new Handler(Looper.getMainLooper())); createPlayers(manager.getActiveSessions(new ComponentName(context, NotificationReceiver.class))); sendPlayerList(); } catch (Exception e) { Log.e(TAG, "Exception", e); } return true; } private void createPlayers(List sessions) { for (MediaController controller : sessions) { createPlayer(controller); } } @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_mprisreceiver); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_mprisreceiver_desc); } @Override public boolean onPacketReceived(NetworkPacket np) { if (np.getBoolean("requestPlayerList")) { sendPlayerList(); return true; } if (!np.has("player")) { return false; } MprisReceiverPlayer player = players.get(np.getString("player")); if (null == player) { return false; } if (np.getBoolean("requestNowPlaying", false)) { sendMetadata(player); return true; } if (np.has("action")) { String action = np.getString("action"); switch (action) { case "PlayPause": player.playPause(); break; case "Next": player.next(); break; case "Previous": player.previous(); } } return true; } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_MPRIS_REQUEST}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_MPRIS}; } @Override public void onActiveSessionsChanged(@Nullable List controllers) { if (null == controllers) { return; } players.clear(); createPlayers(controllers); sendPlayerList(); } private void createPlayer(MediaController controller) { MprisReceiverPlayer player = new MprisReceiverPlayer(controller, AppsHelper.appNameLookup(context, controller.getPackageName())); controller.registerCallback(new MprisReceiverCallback(this, player), new Handler(Looper.getMainLooper())); players.put(player.getName(), player); } private void sendPlayerList() { NetworkPacket np = new NetworkPacket(PACKET_TYPE_MPRIS); np.set("playerList", players.keySet()); device.sendPacket(np); } void sendPlaying(MprisReceiverPlayer player) { NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); np.set("player", player.getName()); np.set("isPlaying", player.isPlaying()); device.sendPacket(np); } @Override public int getMinSdk() { return Build.VERSION_CODES.LOLLIPOP_MR1; } public void sendMetadata(MprisReceiverPlayer player) { NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); np.set("player", player.getName()); if (player.getArtist().isEmpty()) { np.set("nowPlaying", player.getTitle()); } else { np.set("nowPlaying", player.getArtist() + " - " + player.getTitle()); } np.set("title", player.getTitle()); np.set("artist", player.getArtist()); np.set("album", player.getAlbum()); np.set("isPlaying", player.isPlaying()); np.set("pos", player.getPosition()); device.sendPacket(np); } public void sendVolume(MprisReceiverPlayer player) { NetworkPacket np = new NetworkPacket(MprisReceiverPlugin.PACKET_TYPE_MPRIS); np.set("player", player.getName()); np.set("volume", player.getVolume()); device.sendPacket(np); } @Override public AlertDialog getErrorDialog(final Activity deviceActivity) { return new AlertDialog.Builder(deviceActivity) .setTitle(R.string.pref_plugin_mpris) .setMessage(R.string.no_permission_mprisreceiver) .setPositiveButton(R.string.open_settings, (dialogInterface, i) -> { Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); deviceActivity.startActivityForResult(intent, MainActivity.RESULT_NEEDS_RELOAD); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { //Do nothing }) .create(); } private boolean hasPermission() { String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners"); return (notificationListenerList != null && notificationListenerList.contains(context.getPackageName())); } } diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java index 9681c44c..4119d599 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java @@ -1,247 +1,247 @@ /* * Copyright 2015 Vineet Garg * * 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.NotificationsPlugin; -import android.app.AlertDialog; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.CheckedTextView; import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.UserInterface.ThemeUtil; import org.kde.kdeconnect_tp.R; import java.util.Arrays; import java.util.List; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; //TODO: Turn this into a PluginSettingsFragment public class NotificationFilterActivity extends AppCompatActivity { private AppDatabase appDatabase; private ListView listView; static class AppListInfo { String pkg; String name; Drawable icon; boolean isEnabled; } private AppListInfo[] apps; class AppListAdapter extends BaseAdapter { @Override public int getCount() { return apps.length + 1; } @Override public AppListInfo getItem(int position) { return apps[position - 1]; } @Override public long getItemId(int position) { return position - 1; } public View getView(int position, View view, ViewGroup parent) { if (view == null) { LayoutInflater inflater = getLayoutInflater(); view = inflater.inflate(android.R.layout.simple_list_item_multiple_choice, null, true); } CheckedTextView checkedTextView = (CheckedTextView) view; if (position == 0) { checkedTextView.setText(R.string.all); checkedTextView.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null); } else { checkedTextView.setText(apps[position - 1].name); checkedTextView.setCompoundDrawablesWithIntrinsicBounds(apps[position - 1].icon, null, null, null); checkedTextView.setCompoundDrawablePadding((int) (8 * getResources().getDisplayMetrics().density)); } return view; } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_notification_filter); appDatabase = new AppDatabase(NotificationFilterActivity.this, false); new Thread(() -> { PackageManager packageManager = getPackageManager(); List appList = packageManager.getInstalledApplications(0); int count = appList.size(); apps = new AppListInfo[count]; for (int i = 0; i < count; i++) { ApplicationInfo appInfo = appList.get(i); apps[i] = new AppListInfo(); apps[i].pkg = appInfo.packageName; apps[i].name = appInfo.loadLabel(packageManager).toString(); apps[i].icon = resizeIcon(appInfo.loadIcon(packageManager), 48); apps[i].isEnabled = appDatabase.isEnabled(appInfo.packageName); } Arrays.sort(apps, (lhs, rhs) -> lhs.name.compareToIgnoreCase(rhs.name)); runOnUiThread(this::displayAppList); }).start(); } private void displayAppList() { listView = findViewById(R.id.lvFilterApps); AppListAdapter adapter = new AppListAdapter(); listView.setAdapter(adapter); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); listView.setLongClickable(true); listView.setOnItemClickListener((adapterView, view, i, l) -> { if (i == 0) { boolean enabled = listView.isItemChecked(0); for (int j = 0; j < apps.length; j++) { listView.setItemChecked(j, enabled); } appDatabase.setAllEnabled(enabled); } else { boolean checked = listView.isItemChecked(i); appDatabase.setEnabled(apps[i - 1].pkg, checked); apps[i - 1].isEnabled = checked; } }); listView.setOnItemLongClickListener((adapterView, view, i, l) -> { if(i == 0) return true; Context context = this; AlertDialog.Builder builder = new AlertDialog.Builder(context); View mView = getLayoutInflater().inflate(R.layout.popup_notificationsfilter, null); builder.setMessage(context.getResources().getString(R.string.extra_options)); ListView lv = mView.findViewById(R.id.extra_options_list); final String[] options = new String[] { context.getResources().getString(R.string.privacy_options) }; ArrayAdapter extra_options_adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options); lv.setAdapter(extra_options_adapter); builder.setView(mView); AlertDialog ad = builder.create(); lv.setOnItemClickListener((new_adapterView, new_view, new_i, new_l) -> { switch (new_i){ case 0: AlertDialog.Builder myBuilder = new AlertDialog.Builder(context); String packageName = apps[i - 1].pkg; View myView = getLayoutInflater().inflate(R.layout.privacy_options, null); CheckBox checkbox_contents = myView.findViewById(R.id.checkbox_contents); checkbox_contents.setChecked(appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)); checkbox_contents.setText(context.getResources().getString(R.string.block_contents)); CheckBox checkbox_images = myView.findViewById(R.id.checkbox_images); checkbox_images.setChecked(appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES)); checkbox_images.setText(context.getResources().getString(R.string.block_images)); myBuilder.setView(myView); myBuilder.setTitle(context.getResources().getString(R.string.privacy_options)); myBuilder.setPositiveButton(context.getResources().getString(R.string.ok), (dialog, id) -> dialog.dismiss()); myBuilder.setMessage(context.getResources().getString(R.string.set_privacy_options)); checkbox_contents.setOnCheckedChangeListener((compoundButton, b) -> appDatabase.setPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS, compoundButton.isChecked())); checkbox_images.setOnCheckedChangeListener((compoundButton, b) -> appDatabase.setPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES, compoundButton.isChecked())); ad.cancel(); myBuilder.create().show(); break; } }); ad.show(); return true; }); listView.setItemChecked(0, appDatabase.getAllEnabled()); //"Select all" button for (int i = 0; i < apps.length; i++) { listView.setItemChecked(i + 1, apps[i].isEnabled); } listView.setVisibility(View.VISIBLE); findViewById(R.id.spinner).setVisibility(View.GONE); } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } private Drawable resizeIcon(Drawable icon, int maxSize) { Resources res = getResources(); //Convert to display pixels maxSize = (int) (maxSize * res.getDisplayMetrics().density); Bitmap bitmap = Bitmap.createBitmap(maxSize, maxSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); icon.draw(canvas); return new BitmapDrawable(res, bitmap); } } diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java index 42612e78..69ab6e10 100644 --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -1,588 +1,588 @@ /* * 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.NotificationsPlugin; import android.annotation.TargetApi; import android.app.Activity; -import android.app.AlertDialog; import android.app.Notification; import android.app.PendingIntent; import android.app.RemoteInput; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.Drawable; import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.text.SpannableString; 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.MainActivity; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import java.io.ByteArrayOutputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; import androidx.core.app.NotificationCompat; @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) public class NotificationsPlugin extends Plugin implements NotificationReceiver.NotificationListener { private final static String PACKET_TYPE_NOTIFICATION = "kdeconnect.notification"; private final static String PACKET_TYPE_NOTIFICATION_REQUEST = "kdeconnect.notification.request"; private final static String PACKET_TYPE_NOTIFICATION_REPLY = "kdeconnect.notification.reply"; private AppDatabase appDatabase; private Set currentNotifications; private Map pendingIntents; private boolean serviceReady; @Override public String getDisplayName() { return context.getResources().getString(R.string.pref_plugin_notifications); } @Override public String getDescription() { return context.getResources().getString(R.string.pref_plugin_notifications_desc); } @Override public boolean hasSettings() { return true; } @Override public PluginSettingsFragment getSettingsFragment() { if (hasPermission()) { Context context = device.getContext(); Intent intent = new Intent(context, NotificationFilterActivity.class); context.startActivity(intent); } return null; } private boolean hasPermission() { String notificationListenerList = Settings.Secure.getString(context.getContentResolver(), "enabled_notification_listeners"); return (notificationListenerList != null && notificationListenerList.contains(context.getPackageName())); } @Override public boolean onCreate() { if (!hasPermission()) return false; pendingIntents = new HashMap<>(); currentNotifications = new HashSet<>(); appDatabase = new AppDatabase(context, true); NotificationReceiver.RunCommand(context, service -> { service.addListener(NotificationsPlugin.this); serviceReady = service.isConnected(); if (serviceReady) { sendCurrentNotifications(service); } }); return true; } @Override public void onDestroy() { NotificationReceiver.RunCommand(context, service -> service.removeListener(NotificationsPlugin.this)); } @Override public void onListenerConnected(NotificationReceiver service) { serviceReady = true; sendCurrentNotifications(service); } @Override public void onNotificationRemoved(StatusBarNotification statusBarNotification) { if (statusBarNotification == null) { Log.w("onNotificationRemoved", "notification is null"); return; } String id = getNotificationKeyCompat(statusBarNotification); NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); np.set("id", id); np.set("isCancel", true); device.sendPacket(np); currentNotifications.remove(id); } @Override public void onNotificationPosted(StatusBarNotification statusBarNotification) { sendNotification(statusBarNotification); } private void sendNotification(StatusBarNotification statusBarNotification) { Notification notification = statusBarNotification.getNotification(); if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0 || (notification.flags & Notification.FLAG_ONGOING_EVENT) != 0 || (notification.flags & Notification.FLAG_LOCAL_ONLY) != 0 || (notification.flags & NotificationCompat.FLAG_GROUP_SUMMARY) != 0) //The notification that groups other notifications { //This is not a notification we want! return; } if (!appDatabase.isEnabled(statusBarNotification.getPackageName())) { return; // we dont want notification from this app } String key = getNotificationKeyCompat(statusBarNotification); String packageName = statusBarNotification.getPackageName(); String appName = AppsHelper.appNameLookup(context, packageName); if ("com.facebook.orca".equals(packageName) && (statusBarNotification.getId() == 10012) && "Messenger".equals(appName) && notification.tickerText == null) { //HACK: Hide weird Facebook empty "Messenger" notification that is actually not shown in the phone return; } if ("com.android.systemui".equals(packageName) && "low_battery".equals(statusBarNotification.getTag())) { //HACK: Android low battery notification are posted again every few seconds. Ignore them, as we already have a battery indicator. return; } if ("org.kde.kdeconnect_tp".equals(packageName)) { // Don't send our own notifications return; } NetworkPacket np = new NetworkPacket(PACKET_TYPE_NOTIFICATION); boolean isUpdate = currentNotifications.contains(key); if (!isUpdate) { //If it's an update, the other end should have the icon already: no need to extract it and create the payload again try { Bitmap appIcon; Context foreignContext = context.createPackageContext(statusBarNotification.getPackageName(), 0); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { appIcon = iconToBitmap(foreignContext, notification.getLargeIcon()); } else { appIcon = notification.largeIcon; } if (appIcon == null) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) { appIcon = iconToBitmap(foreignContext, notification.getSmallIcon()); } else { PackageManager pm = context.getPackageManager(); Resources foreignResources = pm.getResourcesForApplication(statusBarNotification.getPackageName()); Drawable foreignIcon = foreignResources.getDrawable(notification.icon); appIcon = drawableToBitmap(foreignIcon); } } if (appIcon != null && !appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_IMAGES)) { ByteArrayOutputStream outStream = new ByteArrayOutputStream(); appIcon.compress(Bitmap.CompressFormat.PNG, 90, outStream); byte[] bitmapData = outStream.toByteArray(); Log.e("PAYLOAD", "PAYLOAD: " + getChecksum(bitmapData)); np.setPayload(new NetworkPacket.Payload(bitmapData)); np.set("payloadHash", getChecksum(bitmapData)); } } catch (Exception e) { e.printStackTrace(); Log.e("NotificationsPlugin", "Error retrieving icon"); } } else { currentNotifications.add(key); } np.set("id", key); np.set("isClearable", statusBarNotification.isClearable()); np.set("appName", appName == null ? packageName : appName); np.set("time", Long.toString(statusBarNotification.getPostTime())); if (!appDatabase.getPrivacy(packageName, AppDatabase.PrivacyOptions.BLOCK_CONTENTS)) { RepliableNotification rn = extractRepliableNotification(statusBarNotification); if (rn.pendingIntent != null) { np.set("requestReplyId", rn.id); pendingIntents.put(rn.id, rn); } np.set("ticker", getTickerText(notification)); np.set("title", getNotificationTitle(notification)); np.set("text", getNotificationText(notification)); } device.sendPacket(np); } private Bitmap drawableToBitmap(Drawable drawable) { if (drawable == null) return null; Bitmap res; if (drawable.getIntrinsicWidth() > 128 || drawable.getIntrinsicHeight() > 128) { res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); } else if (drawable.getIntrinsicWidth() <= 64 || drawable.getIntrinsicHeight() <= 64) { res = Bitmap.createBitmap(96, 96, Bitmap.Config.ARGB_8888); } else { res = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); } Canvas canvas = new Canvas(res); drawable.setBounds(0, 0, res.getWidth(), res.getHeight()); drawable.draw(canvas); return res; } @RequiresApi(Build.VERSION_CODES.M) private Bitmap iconToBitmap(Context foreignContext, Icon icon) { if (icon == null) return null; return drawableToBitmap(icon.loadDrawable(foreignContext)); } @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private void replyToNotification(String id, String message) { if (pendingIntents.isEmpty() || !pendingIntents.containsKey(id)) { Log.e("NotificationsPlugin", "No such notification"); return; } RepliableNotification repliableNotification = pendingIntents.get(id); if (repliableNotification == null) { Log.e("NotificationsPlugin", "No such notification"); return; } RemoteInput[] remoteInputs = new RemoteInput[repliableNotification.remoteInputs.size()]; Intent localIntent = new Intent(); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Bundle localBundle = new Bundle(); int i = 0; for (RemoteInput remoteIn : repliableNotification.remoteInputs) { getDetailsOfNotification(remoteIn); remoteInputs[i] = remoteIn; localBundle.putCharSequence(remoteInputs[i].getResultKey(), message); i++; } RemoteInput.addResultsToIntent(remoteInputs, localIntent, localBundle); try { repliableNotification.pendingIntent.send(context, 0, localIntent); } catch (PendingIntent.CanceledException e) { Log.e("NotificationPlugin", "replyToNotification error: " + e.getMessage()); } pendingIntents.remove(id); } @RequiresApi(api = Build.VERSION_CODES.KITKAT_WATCH) private void getDetailsOfNotification(RemoteInput remoteInput) { //Some more details of RemoteInput... no idea what for but maybe it will be useful at some point String resultKey = remoteInput.getResultKey(); String label = remoteInput.getLabel().toString(); Boolean canFreeForm = remoteInput.getAllowFreeFormInput(); if (remoteInput.getChoices() != null && remoteInput.getChoices().length > 0) { String[] possibleChoices = new String[remoteInput.getChoices().length]; for (int i = 0; i < remoteInput.getChoices().length; i++) { possibleChoices[i] = remoteInput.getChoices()[i].toString(); } } } private String getNotificationTitle(Notification notification) { final String TITLE_KEY = "android.title"; final String TEXT_KEY = "android.text"; String title = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; title = extractStringFromExtra(extras, TITLE_KEY); } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } } return title; } private RepliableNotification extractRepliableNotification(StatusBarNotification statusBarNotification) { RepliableNotification repliableNotification = new RepliableNotification(); if (statusBarNotification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { try { if (statusBarNotification.getNotification().actions != null) { for (Notification.Action act : statusBarNotification.getNotification().actions) { if (act != null && act.getRemoteInputs() != null) { // Is a reply repliableNotification.remoteInputs.addAll(Arrays.asList(act.getRemoteInputs())); repliableNotification.pendingIntent = act.actionIntent; break; } } repliableNotification.packageName = statusBarNotification.getPackageName(); repliableNotification.tag = statusBarNotification.getTag();//TODO find how to pass Tag with sending PendingIntent, might fix Hangout problem } } catch (Exception e) { Log.w("NotificationPlugin", "problem extracting notification wear for " + statusBarNotification.getNotification().tickerText); e.printStackTrace(); } } } return repliableNotification; } private String getNotificationText(Notification notification) { final String TEXT_KEY = "android.text"; String text = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; Object extraTextExtra = extras.get(TEXT_KEY); if (extraTextExtra != null) text = extraTextExtra.toString(); } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } } return text; } private static String extractStringFromExtra(Bundle extras, String key) { Object extra = extras.get(key); if (extra == null) { return null; } else if (extra instanceof String) { return (String) extra; } else if (extra instanceof SpannableString) { return extra.toString(); } else { Log.e("NotificationsPlugin", "Don't know how to extract text from extra of type: " + extra.getClass().getCanonicalName()); return null; } } /** * Returns the ticker text of the notification. * If device android version is KitKat or newer, the title and text of the notification is used * instead the ticker text. */ private String getTickerText(Notification notification) { final String TITLE_KEY = "android.title"; final String TEXT_KEY = "android.text"; String ticker = ""; if (notification != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { try { Bundle extras = notification.extras; String extraTitle = extractStringFromExtra(extras, TITLE_KEY); String extraText = extractStringFromExtra(extras, TEXT_KEY); if (extraTitle != null && extraText != null && !extraText.isEmpty()) { ticker = extraTitle + ": " + extraText; } else if (extraTitle != null) { ticker = extraTitle; } else if (extraText != null) { ticker = extraText; } } catch (Exception e) { Log.w("NotificationPlugin", "problem parsing notification extras for " + notification.tickerText); e.printStackTrace(); } } if (ticker.isEmpty()) { ticker = (notification.tickerText != null) ? notification.tickerText.toString() : ""; } } return ticker; } private void sendCurrentNotifications(NotificationReceiver service) { StatusBarNotification[] notifications = service.getActiveNotifications(); for (StatusBarNotification notification : notifications) { sendNotification(notification); } } @Override public boolean onPacketReceived(final NetworkPacket np) { if (np.getBoolean("request")) { if (serviceReady) { NotificationReceiver.RunCommand(context, this::sendCurrentNotifications); } } else if (np.has("cancel")) { final String dismissedId = np.getString("cancel"); currentNotifications.remove(dismissedId); NotificationReceiver.RunCommand(context, service -> cancelNotificationCompat(service, dismissedId)); } else if (np.has("requestReplyId") && np.has("message")) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { replyToNotification(np.getString("requestReplyId"), np.getString("message")); } } return true; } @Override public AlertDialog getErrorDialog(final Activity deviceActivity) { return new AlertDialog.Builder(deviceActivity) .setTitle(R.string.pref_plugin_notifications) .setMessage(R.string.no_permissions) .setPositiveButton(R.string.open_settings, (dialogInterface, i) -> { Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); deviceActivity.startActivityForResult(intent, MainActivity.RESULT_NEEDS_RELOAD); }) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { //Do nothing }) .create(); } @Override public String[] getSupportedPacketTypes() { return new String[]{PACKET_TYPE_NOTIFICATION_REQUEST, PACKET_TYPE_NOTIFICATION_REPLY}; } @Override public String[] getOutgoingPacketTypes() { return new String[]{PACKET_TYPE_NOTIFICATION}; } //For compat with API<21, because lollipop changed the way to cancel notifications private static void cancelNotificationCompat(NotificationReceiver service, String compatKey) { if (Build.VERSION.SDK_INT >= 21) { service.cancelNotification(compatKey); } else { int first = compatKey.indexOf(':'); if (first == -1) { Log.e("cancelNotificationCompa", "Not formatted like a notification key: " + compatKey); return; } int last = compatKey.lastIndexOf(':'); String packageName = compatKey.substring(0, first); String tag = compatKey.substring(first + 1, last); if (tag.length() == 0) tag = null; String idString = compatKey.substring(last + 1); int id; try { id = Integer.parseInt(idString); } catch (Exception e) { id = 0; } service.cancelNotification(packageName, tag, id); } } private static String getNotificationKeyCompat(StatusBarNotification statusBarNotification) { String result; // first check if it's one of our remoteIds String tag = statusBarNotification.getTag(); if (tag != null && tag.startsWith("kdeconnectId:")) result = Integer.toString(statusBarNotification.getId()); else if (Build.VERSION.SDK_INT >= 21) { result = statusBarNotification.getKey(); } else { String packageName = statusBarNotification.getPackageName(); int id = statusBarNotification.getId(); String safePackageName = (packageName == null) ? "" : packageName; String safeTag = (tag == null) ? "" : tag; result = safePackageName + ":" + safeTag + ":" + id; } return result; } private String getChecksum(byte[] data) { try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(data); return bytesToHex(md.digest()); } catch (NoSuchAlgorithmException e) { Log.e("KDEConnect", "Error while generating checksum", e); } return null; } private static String bytesToHex(byte[] bytes) { char[] hexArray = "0123456789ABCDEF".toCharArray(); char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = hexArray[v >>> 4]; hexChars[j * 2 + 1] = hexArray[v & 0x0F]; } return new String(hexChars).toLowerCase(); } @Override public int getMinSdk() { return Build.VERSION_CODES.JELLY_BEAN_MR2; } } diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java index 23a2ca65..b226426e 100644 --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -1,270 +1,270 @@ /* * 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.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.drawable.Drawable; import android.os.Build; import android.widget.Button; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; public abstract class Plugin { protected Device device; protected Context context; protected int permissionExplanation = R.string.permission_explanation; protected int optionalPermissionExplanation = R.string.optional_permission_explanation; public final void setContext(Context context, Device device) { this.device = device; this.context = context; } /** * To receive the network package from the unpaired device, override * listensToUnpairedDevices to return true and this method. */ public boolean onUnpairedDevicePacketReceived(NetworkPacket np) { return false; } /** * Returns whether this plugin should be loaded or not, to listen to NetworkPackets * from the unpaired devices. By default, returns false. */ public boolean listensToUnpairedDevices() { return false; } /** * Return the internal plugin name, that will be used as a * unique key to distinguish it. Use the class name as key. */ public String getPluginKey() { return getPluginKey(this.getClass()); } public static String getPluginKey(Class p) { return p.getSimpleName(); } /** * Return the human-readable plugin name. This function can * access this.context to provide translated text. */ public abstract String getDisplayName(); /** * Return the human-readable description of this plugin. This * function can access this.context to provide translated text. */ public abstract String getDescription(); /** * Return the action name displayed in the main activity, that * will call startMainActivity when clicked */ public String getActionName() { return getDisplayName(); } /** * Return an icon associated to this plugin. This function can * access this.context to load the image from resources. */ public Drawable getIcon() { return null; } /** * Return true if this plugin should be enabled on new devices. * This function can access this.context and perform compatibility * checks with the Android version, but can not access this.device. */ public boolean isEnabledByDefault() { return true; } /** * Return true if this plugin needs an specific UI settings. */ public boolean hasSettings() { return false; } /** * If hasSettings returns true, this will be called when the user * wants to access this plugin's preferences. The default implementation * will return a PluginSettingsFragment with content from "yourplugin"_preferences.xml * * @return The PluginSettingsFragment used to display this plugins settings */ public PluginSettingsFragment getSettingsFragment() { return PluginSettingsFragment.newInstance(getPluginKey()); } /** * Return true if the plugin should display something in the Device main view */ public boolean hasMainActivity() { return false; } /** * Implement here what your plugin should do when clicked */ public void startMainActivity(Activity parentActivity) { } /** * Return true if the entry for this app should appear in the context menu instead of the main view */ public boolean displayInContextMenu() { return false; } /** * Initialize the listeners and structures in your plugin. * Should return true if initialization was successful. */ public boolean onCreate() { return true; } /** * Finish any ongoing operations, remove listeners... so * this object could be garbage collected. */ public void onDestroy() { } /** * Called when a plugin receives a package. By convention we return true * when we have done something in response to the package or false * otherwise, even though that value is unused as of now. */ public boolean onPacketReceived(NetworkPacket np) { return false; } /** * Should return the list of NetworkPacket types that this plugin can handle */ public abstract String[] getSupportedPacketTypes(); /** * Should return the list of NetworkPacket types that this plugin can send */ public abstract String[] getOutgoingPacketTypes(); /** * Creates a button that will be displayed in the user interface * It can open an activity or perform any other action that the * plugin would wants to expose to the user. Return null if no * button should be displayed. */ @Deprecated public Button getInterfaceButton(final Activity activity) { if (!hasMainActivity()) return null; Button b = new Button(activity); b.setText(getActionName()); b.setOnClickListener(view -> startMainActivity(activity)); return b; } protected String[] getRequiredPermissions() { return new String[0]; } protected String[] getOptionalPermissions() { return new String[0]; } //Permission from Manifest.permission.* protected boolean isPermissionGranted(String permission) { int result = ContextCompat.checkSelfPermission(context, permission); return (result == PackageManager.PERMISSION_GRANTED); } private boolean arePermissionsGranted(String[] permissions) { for (String permission : permissions) { if (!isPermissionGranted(permission)) { return false; } } return true; } protected AlertDialog requestPermissionDialog(Activity activity, String permissions, @StringRes int reason) { return requestPermissionDialog(activity, new String[]{permissions}, reason); } private AlertDialog requestPermissionDialog(final Activity activity, final String[] permissions, @StringRes int reason) { return new AlertDialog.Builder(activity) .setTitle(getDisplayName()) .setMessage(reason) .setPositiveButton(R.string.ok, (dialogInterface, i) -> ActivityCompat.requestPermissions(activity, permissions, 0)) .setNegativeButton(R.string.cancel, (dialogInterface, i) -> { //Do nothing }) .create(); } /** * If onCreate returns false, should create a dialog explaining * the problem (and how to fix it, if possible) to the user. */ public AlertDialog getErrorDialog(Activity deviceActivity) { return null; } public AlertDialog getPermissionExplanationDialog(Activity deviceActivity) { return requestPermissionDialog(deviceActivity, getRequiredPermissions(), permissionExplanation); } public AlertDialog getOptionalPermissionExplanationDialog(Activity deviceActivity) { return requestPermissionDialog(deviceActivity, getOptionalPermissions(), optionalPermissionExplanation); } public boolean checkRequiredPermissions() { return arePermissionsGranted(getRequiredPermissions()); } public boolean checkOptionalPermissions() { return arePermissionsGranted(getOptionalPermissions()); } public int getMinSdk() { return Build.VERSION_CODES.BASE; } } diff --git a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java index b2cfdbe1..3d52b84e 100644 --- a/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/CustomDevicesActivity.java @@ -1,170 +1,170 @@ /* * Copyright 2014 Achilleas Koutsou * * 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.UserInterface; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.text.TextUtils; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ListView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collections; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; public class CustomDevicesActivity extends AppCompatActivity { public static final String KEY_CUSTOM_DEVLIST_PREFERENCE = "device_list_preference"; private static final String IP_DELIM = ","; private ListView list; private ArrayList ipAddressList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); initializeDeviceList(this); ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.custom_ip_list); list = findViewById(android.R.id.list); list.setOnItemClickListener(onClickListener); list.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, ipAddressList)); findViewById(android.R.id.button1).setOnClickListener(v -> addNewDevice()); EditText ipEntryBox = findViewById(R.id.ip_edittext); ipEntryBox.setOnEditorActionListener((v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND) { addNewDevice(); return true; } return false; }); } private boolean dialogAlreadyShown = false; private final AdapterView.OnItemClickListener onClickListener = (parent, view, position, id) -> { if (dialogAlreadyShown) { return; } // remove touched item after confirmation DialogInterface.OnClickListener confirmationListener = (dialog, which) -> { switch (which) { case DialogInterface.BUTTON_POSITIVE: ipAddressList.remove(position); saveList(); break; case DialogInterface.BUTTON_NEGATIVE: break; } }; AlertDialog.Builder builder = new AlertDialog.Builder(CustomDevicesActivity.this); builder.setMessage(getString(R.string.delete_custom_device, ipAddressList.get(position))); builder.setPositiveButton(R.string.ok, confirmationListener); builder.setNegativeButton(R.string.cancel, confirmationListener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { //DismissListener dialogAlreadyShown = true; builder.setOnDismissListener(dialog -> dialogAlreadyShown = false); } builder.show(); }; private void addNewDevice() { EditText ipEntryBox = findViewById(R.id.ip_edittext); String enteredText = ipEntryBox.getText().toString().trim(); if (!enteredText.isEmpty()) { // don't add empty string (after trimming) ipAddressList.add(enteredText); } saveList(); // clear entry box ipEntryBox.setText(""); InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); View focus = getCurrentFocus(); if (focus != null && inputManager != null) { inputManager.hideSoftInputFromWindow(focus.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS); } } private void saveList() { String serialized = TextUtils.join(IP_DELIM, ipAddressList); PreferenceManager.getDefaultSharedPreferences(CustomDevicesActivity.this).edit().putString( KEY_CUSTOM_DEVLIST_PREFERENCE, serialized).apply(); ((ArrayAdapter) list.getAdapter()).notifyDataSetChanged(); } public static ArrayList deserializeIpList(String serialized) { ArrayList ipList = new ArrayList<>(); Collections.addAll(ipList, serialized.split(IP_DELIM)); return ipList; } private void initializeDeviceList(Context context) { String deviceListPrefs = PreferenceManager.getDefaultSharedPreferences(context).getString( KEY_CUSTOM_DEVLIST_PREFERENCE, ""); if (deviceListPrefs.isEmpty()) { PreferenceManager.getDefaultSharedPreferences(context).edit().putString( KEY_CUSTOM_DEVLIST_PREFERENCE, deviceListPrefs).apply(); } else { ipAddressList = deserializeIpList(deviceListPrefs); } } @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this); } @Override protected void onStop() { super.onStop(); BackgroundService.removeGuiInUseCounter(this); } } diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java index 3ddc96dd..e6262a3f 100644 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java @@ -1,439 +1,439 @@ /* * 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.UserInterface; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.ListView; import android.widget.ProgressBar; import android.widget.TextView; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Helpers.NetworkHelper; import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.UserInterface.List.CustomItem; import org.kde.kdeconnect.UserInterface.List.FailedPluginListItem; import org.kde.kdeconnect.UserInterface.List.ListAdapter; import org.kde.kdeconnect.UserInterface.List.PluginItem; import org.kde.kdeconnect.UserInterface.List.SmallEntryItem; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; import java.util.Collection; import java.util.ConcurrentModificationException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; import androidx.fragment.app.Fragment; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; import butterknife.Unbinder; /** * Main view. Displays the current device and its plugins */ public class DeviceFragment extends Fragment { private static final String ARG_DEVICE_ID = "deviceId"; private static final String ARG_FROM_DEVICE_LIST = "fromDeviceList"; private View rootView; private String mDeviceId; private Device device; private MainActivity mActivity; private ArrayList pluginListItems; @BindView(R.id.pair_button) Button pairButton; @BindView(R.id.accept_button) Button acceptButton; @BindView(R.id.reject_button) Button rejectButton; @BindView(R.id.pair_message) TextView pairMessage; @BindView(R.id.pair_progress) ProgressBar pairProgress; @BindView(R.id.pairing_buttons) View pairingButtons; @BindView(R.id.pair_request_buttons) View pairRequestButtons; @BindView(R.id.error_message_container) View errorMessageContainer; @BindView(R.id.not_reachable_message) TextView notReachableMessage; @BindView(R.id.on_data_message) TextView onDataMessage; @BindView(R.id.buttons_list) ListView buttonsList; private Unbinder unbinder; public DeviceFragment() { } public static DeviceFragment newInstance(String deviceId, boolean fromDeviceList) { DeviceFragment frag = new DeviceFragment(); Bundle args = new Bundle(); args.putString(ARG_DEVICE_ID, deviceId); args.putBoolean(ARG_FROM_DEVICE_LIST, fromDeviceList); frag.setArguments(args); return frag; } @Override public void onAttach(Activity activity) { super.onAttach(activity); mActivity = ((MainActivity) getActivity()); } @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() == null || !getArguments().containsKey(ARG_DEVICE_ID)) { throw new RuntimeException("You must instantiate a new DeviceFragment using DeviceFragment.newInstance()"); } mDeviceId = getArguments().getString(ARG_DEVICE_ID); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { rootView = inflater.inflate(R.layout.activity_device, container, false); unbinder = ButterKnife.bind(this, rootView); setHasOptionsMenu(true); //Log.e("DeviceFragment", "device: " + deviceId); BackgroundService.RunCommand(mActivity, service -> { device = service.getDevice(mDeviceId); if (device == null) { Log.e("DeviceFragment", "Trying to display a device fragment but the device is not present"); mActivity.onDeviceSelected(null); return; } mActivity.getSupportActionBar().setTitle(device.getName()); device.addPairingCallback(pairingCallback); device.addPluginsChangedListener(pluginsChangedListener); refreshUI(); }); return rootView; } String getDeviceId() { return mDeviceId; } private final Device.PluginsChangedListener pluginsChangedListener = device -> refreshUI(); @OnClick(R.id.pair_button) void pairButtonClicked(Button pairButton) { pairButton.setVisibility(View.GONE); pairMessage.setText(""); pairProgress.setVisibility(View.VISIBLE); BackgroundService.RunCommand(mActivity, service -> { device = service.getDevice(mDeviceId); if (device == null) return; device.requestPairing(); }); } @OnClick(R.id.accept_button) void acceptButtonClicked() { if (device != null) { device.acceptPairing(); pairingButtons.setVisibility(View.GONE); } } @OnClick(R.id.reject_button) void setRejectButtonClicked() { if (device != null) { //Remove listener so buttons don't show for a while before changing the view device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); device.rejectPairing(); } mActivity.onDeviceSelected(null); } @Override public void onDestroyView() { BackgroundService.RunCommand(mActivity, service -> { Device device = service.getDevice(mDeviceId); if (device == null) return; device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); }); unbinder.unbind(); super.onDestroyView(); } @Override public void onPrepareOptionsMenu(Menu menu) { //Log.e("DeviceFragment", "onPrepareOptionsMenu"); super.onPrepareOptionsMenu(menu); menu.clear(); if (device == null) { return; } //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.displayInContextMenu()) { continue; } menu.add(p.getActionName()).setOnMenuItemClickListener(item -> { p.startMainActivity(mActivity); return true; }); } menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener(menuItem -> { Intent intent = new Intent(mActivity, DeviceSettingsActivity.class); intent.putExtra("deviceId", mDeviceId); startActivity(intent); return true; }); if (device.isReachable()) { menu.add(R.string.encryption_info_title).setOnMenuItemClickListener(menuItem -> { Context context = mActivity; AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(context.getResources().getString(R.string.encryption_info_title)); builder.setPositiveButton(context.getResources().getString(R.string.ok), (dialog, id) -> dialog.dismiss()); if (device.certificate == null) { builder.setMessage(R.string.encryption_info_msg_no_ssl); } else { builder.setMessage(context.getResources().getString(R.string.my_device_fingerprint) + "\n" + SslHelper.getCertificateHash(SslHelper.certificate) + "\n\n" + context.getResources().getString(R.string.remote_device_fingerprint) + "\n" + SslHelper.getCertificateHash(device.certificate)); } builder.create().show(); return true; }); } if (device.isPaired()) { menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener(menuItem -> { //Remove listener so buttons don't show for a while before changing the view device.removePluginsChangedListener(pluginsChangedListener); device.removePairingCallback(pairingCallback); device.unpair(); mActivity.onDeviceSelected(null); return true; }); } } @Override public void onResume() { super.onResume(); getView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { boolean fromDeviceList = getArguments().getBoolean(ARG_FROM_DEVICE_LIST, false); // Handle back button so we go to the list of devices in case we came from there if (fromDeviceList) { mActivity.onDeviceSelected(null); return true; } } return false; }); } private void refreshUI() { //Log.e("DeviceFragment", "refreshUI"); if (device == null || rootView == null) { return; } //Once in-app, there is no point in keep displaying the notification if any device.hidePairingNotification(); mActivity.runOnUiThread(new Runnable() { @Override public void run() { if (device.isPairRequestedByPeer()) { pairMessage.setText(R.string.pair_requested); pairingButtons.setVisibility(View.VISIBLE); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.GONE); pairRequestButtons.setVisibility(View.VISIBLE); } else { boolean paired = device.isPaired(); boolean reachable = device.isReachable(); boolean onData = NetworkHelper.isOnMobileNetwork(DeviceFragment.this.getContext()); pairingButtons.setVisibility(paired ? View.GONE : View.VISIBLE); errorMessageContainer.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); notReachableMessage.setVisibility((paired && !reachable && !onData) ? View.VISIBLE : View.GONE); onDataMessage.setVisibility((paired && !reachable && onData) ? View.VISIBLE : View.GONE); try { pluginListItems = new ArrayList<>(); if (paired && reachable) { //Plugins button list final Collection plugins = device.getLoadedPlugins().values(); for (final Plugin p : plugins) { if (!p.hasMainActivity()) continue; if (p.displayInContextMenu()) continue; pluginListItems.add(new PluginItem(p, v -> p.startMainActivity(mActivity))); } DeviceFragment.this.createPluginsList(device.getFailedPlugins(), R.string.plugins_failed_to_load, (plugin) -> { AlertDialog dialog = plugin.getErrorDialog(mActivity); if (dialog != null) { dialog.show(); } }); DeviceFragment.this.createPluginsList(device.getPluginsWithoutPermissions(), R.string.plugins_need_permission, (plugin) -> { AlertDialog dialog = plugin.getPermissionExplanationDialog(mActivity); if (dialog != null) { dialog.show(); } }); DeviceFragment.this.createPluginsList(device.getPluginsWithoutOptionalPermissions(), R.string.plugins_need_optional_permission, (plugin) -> { AlertDialog dialog = plugin.getOptionalPermissionExplanationDialog(mActivity); if (dialog != null) { dialog.show(); } }); } ListAdapter adapter = new ListAdapter(mActivity, pluginListItems); buttonsList.setAdapter(adapter); mActivity.invalidateOptionsMenu(); } catch (IllegalStateException e) { e.printStackTrace(); //Ignore: The activity was closed while we were trying to update it } catch (ConcurrentModificationException e) { Log.e("DeviceActivity", "ConcurrentModificationException"); this.run(); //Try again } } } }); } private final Device.PairingCallback pairingCallback = new Device.PairingCallback() { @Override public void incomingRequest() { refreshUI(); } @Override public void pairingSuccessful() { refreshUI(); } @Override public void pairingFailed(final String error) { mActivity.runOnUiThread(() -> { if (rootView == null) return; pairMessage.setText(error); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.VISIBLE); pairRequestButtons.setVisibility(View.GONE); refreshUI(); }); } @Override public void unpaired() { mActivity.runOnUiThread(() -> { if (rootView == null) return; pairMessage.setText(R.string.device_not_paired); pairProgress.setVisibility(View.GONE); pairButton.setVisibility(View.VISIBLE); pairRequestButtons.setVisibility(View.GONE); refreshUI(); }); } }; private void createPluginsList(ConcurrentHashMap plugins, int headerText, FailedPluginListItem.Action action) { if (!plugins.isEmpty()) { TextView header = new TextView(mActivity); header.setPadding( ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (28 * getResources().getDisplayMetrics().density)), ((int) (16 * getResources().getDisplayMetrics().density)), ((int) (8 * getResources().getDisplayMetrics().density)) ); header.setOnClickListener(null); header.setOnLongClickListener(null); header.setText(headerText); pluginListItems.add(new CustomItem(header)); for (Map.Entry entry : plugins.entrySet()) { String pluginKey = entry.getKey(); final Plugin plugin = entry.getValue(); if (device.isPluginEnabled(pluginKey)) { if (plugin == null) { pluginListItems.add(new SmallEntryItem(pluginKey)); } else { pluginListItems.add(new FailedPluginListItem(plugin, action)); } } } } } } diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index 71d831d6..40fee48f 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -1,409 +1,409 @@ package org.kde.kdeconnect.UserInterface; import android.app.Activity; -import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Bundle; 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.widget.EditText; import android.widget.TextView; import com.google.android.material.navigation.NavigationView; 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; import java.util.HashSet; import java.util.Set; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.Fragment; import butterknife.BindView; import butterknife.ButterKnife; public class MainActivity extends AppCompatActivity { 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; public static final String PAIR_REQUEST_STATUS = "pair_req_status"; public static final String PAIRING_ACCEPTED = "accepted"; public static final String PAIRING_REJECTED = "rejected"; public static final String PAIRING_PENDING = "pending"; public static final String EXTRA_DEVICE_ID = "deviceId"; @BindView(R.id.navigation_drawer) NavigationView mNavigationView; @BindView(R.id.drawer_layout) DrawerLayout mDrawerLayout; @BindView(R.id.toolbar) Toolbar mToolbar; TextView mNavViewDeviceName; private String mCurrentDevice; private int mCurrentMenuEntry; private SharedPreferences preferences; private final HashMap mMapMenuToDeviceId = new HashMap<>(); @Override protected void onCreate(Bundle savedInstanceState) { // We need to set the theme before the call to 'super.onCreate' below ThemeUtil.setUserPreferredTheme(this); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); View mDrawerHeader = mNavigationView.getHeaderView(0); mNavViewDeviceName = mDrawerHeader.findViewById(R.id.device_name); setSupportActionBar(mToolbar); 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.addDrawerListener(mDrawerToggle); mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); if (actionBar != null) { actionBar.setDisplayHomeAsUpEnabled(true); } mDrawerToggle.setDrawerIndicatorEnabled(true); mDrawerToggle.syncState(); String deviceName = DeviceHelper.getDeviceName(this); mNavViewDeviceName.setText(deviceName); preferences = getSharedPreferences("stored_menu_selection", Context.MODE_PRIVATE); mNavigationView.setNavigationItemSelectedListener(menuItem -> { 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: String deviceId = mMapMenuToDeviceId.get(menuItem); onDeviceSelected(deviceId); break; } mDrawerLayout.closeDrawer(mNavigationView); return true; }); // Decide which menu entry should be selected at start String savedDevice; 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(EXTRA_DEVICE_ID)) { Log.i("MainActivity", "Loading selected device from parameter"); savedDevice = getIntent().getStringExtra(EXTRA_DEVICE_ID); 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); savedDevice = onPairResultFromNotification(savedDevice, pairStatus); if (savedDevice == null) { savedMenuEntry = MENU_ENTRY_ADD_DEVICE; } } } 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; } mCurrentMenuEntry = savedMenuEntry; mCurrentDevice = savedDevice; mNavigationView.setCheckedItem(savedMenuEntry); //FragmentManager will restore whatever fragment was there if (savedInstanceState != null) { Fragment frag = getSupportFragmentManager().findFragmentById(R.id.container); if (!(frag instanceof DeviceFragment) || ((DeviceFragment)frag).getDeviceId().equals(savedDevice)) { return; } } // Activate the chosen fragment and select the entry in the menu if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) { onDeviceSelected(savedDevice); } else { if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS) { setContentFragment(new SettingsFragment()); } else { setContentFragment(new PairingFragment()); } } } private String onPairResultFromNotification(String deviceId, String pairStatus) { assert(deviceId != null); if (!pairStatus.equals(PAIRING_PENDING)) { BackgroundService.RunCommand(this, service -> { Device device = service.getDevice(deviceId); if (device == null) { Log.w("rejectPairing", "Device no longer exists: " + deviceId); return; } if (pairStatus.equals(PAIRING_ACCEPTED)) { device.acceptPairing(); } else if (pairStatus.equals(PAIRING_REJECTED)) { device.rejectPairing(); } }); } if (pairStatus.equals(PAIRING_ACCEPTED) || pairStatus.equals(PAIRING_PENDING)) { return deviceId; } else { return null; } } private int deviceIdToMenuEntryId(String deviceId) { for (HashMap.Entry 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)) { 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 updateDeviceList() { BackgroundService.RunCommand(MainActivity.this, service -> { Menu menu = mNavigationView.getMenu(); menu.clear(); mMapMenuToDeviceId.clear(); SubMenu devicesMenu = menu.addSubMenu(R.string.devices); int id = MENU_ENTRY_DEVICE_FIRST_ID; Collection devices = service.getDevices().values(); for (Device device : devices) { if (device.isReachable() && device.isPaired()) { MenuItem item = devicesMenu.add(Menu.FIRST, id++, 1, device.getName()); item.setIcon(device.getIcon()); item.setCheckable(true); mMapMenuToDeviceId.put(item, device.getDeviceId()); } } 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::updateDeviceList)); updateDeviceList(); } @Override protected void onStop() { BackgroundService.removeGuiInUseCounter(this); BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("MainActivity")); super.onStop(); } 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(); if (mCurrentDevice != null) { mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId); if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) { uncheckAllMenuItems(mNavigationView.getMenu()); } else { mNavigationView.setCheckedItem(mCurrentMenuEntry); } setContentFragment(DeviceFragment.newInstance(deviceId, fromDeviceList)); } else { mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE; mNavigationView.setCheckedItem(mCurrentMenuEntry); setContentFragment(new PairingFragment()); } } private void setContentFragment(Fragment fragment) { 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); outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case RESULT_NEEDS_RELOAD: BackgroundService.RunCommand(this, service -> { Device device = service.getDevice(mCurrentDevice); device.reloadPluginsFromSettings(); }); break; default: super.onActivityResult(requestCode, resultCode, data); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { for (int result : grantResults) { if (result == PackageManager.PERMISSION_GRANTED) { //New permission granted, reload plugins BackgroundService.RunCommand(this, service -> { Device device = service.getDevice(mCurrentDevice); device.reloadPluginsFromSettings(); }); } } } interface NameChangeCallback { void onNameChanged(String newName); } private final Set nameChangeSubscribers = new HashSet<>(); public void addNameChangeCallback(NameChangeCallback cb) { nameChangeSubscribers.add(cb); } public void removeNameChangeCallback(NameChangeCallback cb) { nameChangeSubscribers.remove(cb); } - public void openRenameDeviceDialog() { + public void openRenameDeviceDialog(Context context) { final EditText deviceNameEdit = new EditText(this); String deviceName = DeviceHelper.getDeviceName(this); deviceNameEdit.setText(deviceName); float dpi = this.getResources().getDisplayMetrics().density; deviceNameEdit.setPadding( ((int) (18 * dpi)), ((int) (16 * dpi)), ((int) (18 * dpi)), ((int) (12 * dpi)) ); - new AlertDialog.Builder(this) + new AlertDialog.Builder(context) .setView(deviceNameEdit) .setPositiveButton(R.string.device_rename_confirm, (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(); } private void updateDeviceNameFromMenu(String newDeviceName) { mNavViewDeviceName.setText(newDeviceName); } } diff --git a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java index 6babcef7..3995ee20 100644 --- a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java @@ -1,130 +1,131 @@ package org.kde.kdeconnect.UserInterface; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect_tp.R; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceScreen; import androidx.preference.SwitchPreferenceCompat; import androidx.preference.TwoStatePreference; public class SettingsFragment extends PreferenceFragmentCompat implements MainActivity.NameChangeCallback { private MainActivity mainActivity; private Preference renameDevice; @Override public void onDestroy() { mainActivity.removeNameChangeCallback(this); super.onDestroy(); } @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { mainActivity = (MainActivity)getActivity(); Context context = getPreferenceManager().getContext(); PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); // Rename device mainActivity.addNameChangeCallback(this); + //TODO: Use an EditTextPreference renameDevice = new Preference(context); renameDevice.setPersistent(false); renameDevice.setSelectable(true); renameDevice.setOnPreferenceClickListener(preference -> { - mainActivity.openRenameDeviceDialog(); + mainActivity.openRenameDeviceDialog(context); 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 final TwoStatePreference darkThemeSwitch = new SwitchPreferenceCompat(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); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Preference persistentNotif = new Preference(context); persistentNotif.setTitle(R.string.setting_persistent_notification_oreo); persistentNotif.setSummary(R.string.setting_persistent_notification_description); persistentNotif.setOnPreferenceClickListener(preference -> { Intent intent = new Intent(); intent.setAction("android.settings.APP_NOTIFICATION_SETTINGS"); intent.putExtra("android.provider.extra.APP_PACKAGE", context.getPackageName()); context.startActivity(intent); return true; }); screen.addPreference(persistentNotif); } else { // Persistent notification toggle for Android Versions below Oreo final TwoStatePreference notificationSwitch = new SwitchPreferenceCompat(context); notificationSwitch.setPersistent(false); notificationSwitch.setChecked(NotificationHelper.isPersistentNotificationEnabled(context)); notificationSwitch.setTitle(R.string.setting_persistent_notification); notificationSwitch.setOnPreferenceChangeListener((preference, newValue) -> { final boolean isChecked = (Boolean) newValue; NotificationHelper.setPersistentNotificationEnabled(context, isChecked); BackgroundService.RunCommand(context, service -> service.changePersistentNotificationVisibility(isChecked)); NotificationHelper.setPersistentNotificationEnabled(context, 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); } }