diff --git a/AndroidManifest.xml b/AndroidManifest.xml --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -87,22 +87,6 @@ android:name="android.support.PARENT_ACTIVITY" android:value="org.kde.kdeconnect.UserInterface.MainActivity" /> - - - - - - @@ -268,12 +252,12 @@ + android:parentActivityName="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity"> + android:value="org.kde.kdeconnect.UserInterface.DeviceSettingsActivity" /> \ No newline at end of file diff --git a/res/layout/activity_device_settings.xml b/res/layout/activity_device_settings.xml new file mode 100644 --- /dev/null +++ b/res/layout/activity_device_settings.xml @@ -0,0 +1,7 @@ + + + \ No newline at end of file diff --git a/res/layout/preference_with_button.xml b/res/layout/preference_with_button.xml --- a/res/layout/preference_with_button.xml +++ b/res/layout/preference_with_button.xml @@ -34,17 +34,20 @@ android:id="@android:id/widget_frame" android:layout_width="wrap_content" android:layout_height="match_parent" - android:gravity="center_vertical" - android:orientation="vertical" /> + android:gravity="left|center_vertical|start" + android:minWidth="56dp" + android:orientation="vertical"> + + diff --git a/res/values/strings.xml b/res/values/strings.xml --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -325,4 +325,5 @@ Block images in notifications Notifications from other devices + findmyphone_ringtone diff --git a/res/xml/findmyphoneplugin_preferences.xml b/res/xml/findmyphoneplugin_preferences.xml --- a/res/xml/findmyphoneplugin_preferences.xml +++ b/res/xml/findmyphoneplugin_preferences.xml @@ -3,8 +3,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - \ No newline at end of file diff --git a/src/org/kde/kdeconnect/BackgroundService.java b/src/org/kde/kdeconnect/BackgroundService.java --- a/src/org/kde/kdeconnect/BackgroundService.java +++ b/src/org/kde/kdeconnect/BackgroundService.java @@ -435,5 +435,4 @@ cb.run(plugin); }); } - } diff --git a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java --- a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java +++ b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneActivity.java @@ -80,7 +80,7 @@ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); Uri ringtone; - String ringtoneString = prefs.getString("select_ringtone", ""); + String ringtoneString = prefs.getString(getString(R.string.findmyphone_preference_key_ringtone), ""); if (ringtoneString.isEmpty()) { ringtone = Settings.System.DEFAULT_RINGTONE_URI; } else { diff --git a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhonePlugin.java b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhonePlugin.java --- a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhonePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhonePlugin.java @@ -25,6 +25,7 @@ import org.kde.kdeconnect.Helpers.DeviceHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; public class FindMyPhonePlugin extends Plugin { @@ -75,4 +76,8 @@ return true; } + @Override + public PluginSettingsFragment getSettingsFragment() { + return FindMyPhoneSettingsFragment.newInstance(getPluginKey()); + } } diff --git a/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneSettingsFragment.java b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneSettingsFragment.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/Plugins/FindMyPhonePlugin/FindMyPhoneSettingsFragment.java @@ -0,0 +1,113 @@ +/* + * Copyright 2018 Erik Duisters + * + * 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.FindMyPhonePlugin; + +import android.app.Activity; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; + +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; +import org.kde.kdeconnect_tp.R; + +import androidx.annotation.NonNull; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; + +public class FindMyPhoneSettingsFragment extends PluginSettingsFragment { + private static final int REQUEST_CODE_SELECT_RINGTONE = 1000; + + private String preferenceKeyRingtone; + private SharedPreferences sharedPreferences; + private Preference ringtonePreference; + + public static FindMyPhoneSettingsFragment newInstance(@NonNull String pluginKey) { + FindMyPhoneSettingsFragment fragment = new FindMyPhoneSettingsFragment(); + fragment.setArguments(pluginKey); + + return fragment; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); + + preferenceKeyRingtone = getString(R.string.findmyphone_preference_key_ringtone); + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()); + + ringtonePreference = getPreferenceScreen().findPreference(preferenceKeyRingtone); + + setRingtoneSummary(); + } + + private void setRingtoneSummary() { + String ringtone = sharedPreferences.getString(preferenceKeyRingtone, Settings.System.DEFAULT_RINGTONE_URI.toString()); + + Uri ringtoneUri = Uri.parse(ringtone); + + ringtonePreference.setSummary(RingtoneManager.getRingtone(requireContext(), ringtoneUri).getTitle(requireContext())); + } + + @Override + public boolean onPreferenceTreeClick(Preference preference) { + /* + * There is no RingtonePreference in support library nor androidx, this is the workaround proposed here: + * https://issuetracker.google.com/issues/37057453 + */ + + if (preference.hasKey() && preference.getKey().equals(preferenceKeyRingtone)) { + Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false); + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI); + + String existingValue = sharedPreferences.getString(preferenceKeyRingtone, null); + if (existingValue != null) { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(existingValue)); + } else { + intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Settings.System.DEFAULT_RINGTONE_URI); + } + + startActivityForResult(intent, REQUEST_CODE_SELECT_RINGTONE); + return true; + } + return super.onPreferenceTreeClick(preference); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_CODE_SELECT_RINGTONE && resultCode == Activity.RESULT_OK) { + Uri uri = data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); + + if (uri != null) { + sharedPreferences.edit() + .putString(preferenceKeyRingtone, uri.toString()) + .apply(); + + setRingtoneSummary(); + } + } + } +} diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationFilterActivity.java @@ -48,6 +48,7 @@ import androidx.appcompat.app.AppCompatActivity; +//TODO: Turn this into a PluginSettingsFragment public class NotificationFilterActivity extends AppCompatActivity { private AppDatabase appDatabase; diff --git a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java --- a/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/NotificationsPlugin/NotificationsPlugin.java @@ -44,8 +44,8 @@ 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.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import java.io.ByteArrayOutputStream; @@ -89,13 +89,14 @@ } @Override - public void startPreferencesActivity(final DeviceSettingsActivity parentActivity) { + public PluginSettingsFragment getSettingsFragment() { if (hasPermission()) { - Intent intent = new Intent(parentActivity, NotificationFilterActivity.class); - parentActivity.startActivity(intent); - } else { - getErrorDialog(parentActivity).show(); + Context context = device.getContext(); + Intent intent = new Intent(context, NotificationFilterActivity.class); + context.startActivity(intent); } + + return null; } private boolean hasPermission() { @@ -488,10 +489,8 @@ 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) @@ -503,7 +502,6 @@ //Do nothing }) .create(); - } @Override diff --git a/src/org/kde/kdeconnect/Plugins/Plugin.java b/src/org/kde/kdeconnect/Plugins/Plugin.java --- a/src/org/kde/kdeconnect/Plugins/Plugin.java +++ b/src/org/kde/kdeconnect/Plugins/Plugin.java @@ -23,24 +23,21 @@ import android.app.Activity; import android.app.AlertDialog; import android.content.Context; -import android.content.Intent; 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.DeviceSettingsActivity; -import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import androidx.annotation.StringRes; 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; @@ -125,15 +122,13 @@ /** * 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 - * PluginSettingsActivity with content from "yourplugin"_preferences.xml. + * 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 void startPreferencesActivity(DeviceSettingsActivity parentActivity) { - Intent intent = new Intent(parentActivity, PluginSettingsActivity.class); - intent.putExtra("plugin_display_name", getDisplayName()); - intent.putExtra("plugin_key", getPluginKey()); - parentActivity.startActivity(intent); + public PluginSettingsFragment getSettingsFragment() { + return PluginSettingsFragment.newInstance(getPluginKey()); } /** diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java --- a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardPlugin.java @@ -387,4 +387,8 @@ np.set("state", state); device.sendPacket(np); } + + String getDeviceId() { + return device.getDeviceId(); + } } diff --git a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java --- a/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java +++ b/src/org/kde/kdeconnect/Plugins/RemoteKeyboardPlugin/RemoteKeyboardService.java @@ -33,8 +33,8 @@ import android.view.inputmethod.InputMethodManager; import android.widget.Toast; +import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity; import org.kde.kdeconnect.UserInterface.MainActivity; -import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; import org.kde.kdeconnect_tp.R; import java.util.ArrayList; @@ -162,10 +162,10 @@ if (instances.size() == 1) { // single instance of RemoteKeyboardPlugin -> access its settings RemoteKeyboardPlugin plugin = instances.get(0); if (plugin != null) { - Intent intent = new Intent(this, PluginSettingsActivity.class); + Intent intent = new Intent(this, DeviceSettingsActivity.class); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra("plugin_display_name", plugin.getDisplayName()); - intent.putExtra("plugin_key", plugin.getPluginKey()); + intent.putExtra(DeviceSettingsActivity.EXTRA_DEVICE_ID, plugin.getDeviceId()); + intent.putExtra(DeviceSettingsActivity.EXTRA_PLUGIN_KEY, plugin.getPluginKey()); startActivity(intent); } } else { // != 1 instance of plugin -> show main activity view diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/SharePlugin.java @@ -47,7 +47,7 @@ import org.kde.kdeconnect.Helpers.NotificationHelper; import org.kde.kdeconnect.NetworkPacket; import org.kde.kdeconnect.Plugins.Plugin; -import org.kde.kdeconnect.UserInterface.DeviceSettingsActivity; +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import org.kde.kdeconnect_tp.R; import java.io.BufferedOutputStream; @@ -227,12 +227,12 @@ //We need to check for already existing files only when storing in the default path. //User-defined paths use the new Storage Access Framework that already handles this. //If the file should be opened immediately store it in the standard location to avoid the FileProvider trouble (See ShareNotification::setURI) - if (np.getBoolean("open") || !ShareSettingsActivity.isCustomDestinationEnabled(context)) { - final String defaultPath = ShareSettingsActivity.getDefaultDestinationDirectory().getAbsolutePath(); + if (np.getBoolean("open") || !ShareSettingsFragment.isCustomDestinationEnabled(context)) { + final String defaultPath = ShareSettingsFragment.getDefaultDestinationDirectory().getAbsolutePath(); filename = FilesHelper.findNonExistingNameForNewFile(defaultPath, filename); destinationFolderDocument = DocumentFile.fromFile(new File(defaultPath)); } else { - destinationFolderDocument = ShareSettingsActivity.getDestinationDirectory(context); + destinationFolderDocument = ShareSettingsFragment.getDestinationDirectory(context); } String displayName = FilesHelper.getFileNameWithoutExt(filename); String mimeType = FilesHelper.getMimeTypeFromFile(filename); @@ -273,11 +273,8 @@ } @Override - public void startPreferencesActivity(DeviceSettingsActivity parentActivity) { - Intent intent = new Intent(parentActivity, ShareSettingsActivity.class); - intent.putExtra("plugin_display_name", getDisplayName()); - intent.putExtra("plugin_key", getPluginKey()); - parentActivity.startActivity(intent); + public PluginSettingsFragment getSettingsFragment() { + return ShareSettingsFragment.newInstance(getPluginKey()); } void queuedSendUriList(final ArrayList uriList) { @@ -489,7 +486,7 @@ context.startActivity(intent); } else { - if (!ShareSettingsActivity.isCustomDestinationEnabled(context)) { + if (!ShareSettingsFragment.isCustomDestinationEnabled(context)) { Log.i("SharePlugin", "Adding to downloads"); DownloadManager manager = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE); manager.addCompletedDownload(info.fileDocument.getUri().getLastPathSegment(), device.getName(), true, info.fileDocument.getType(), info.fileDocument.getUri().getPath(), info.fileSize, false); diff --git a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsFragment.java rename from src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java rename to src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsFragment.java --- a/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsActivity.java +++ b/src/org/kde/kdeconnect/Plugins/SharePlugin/ShareSettingsFragment.java @@ -1,3 +1,23 @@ +/* + * Copyright 2016 Richard Wagler + * + * 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.SharePlugin; import android.annotation.TargetApi; @@ -9,32 +29,42 @@ import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.preference.CheckBoxPreference; -import android.preference.Preference; -import android.preference.PreferenceManager; import android.util.Log; -import org.kde.kdeconnect.UserInterface.PluginSettingsActivity; +import org.kde.kdeconnect.UserInterface.PluginSettingsFragment; import java.io.File; +import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; +import androidx.preference.CheckBoxPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceManager; +import androidx.preference.PreferenceScreen; -public class ShareSettingsActivity extends PluginSettingsActivity { +public class ShareSettingsFragment extends PluginSettingsFragment { private final static String PREFERENCE_CUSTOMIZE_DESTINATION = "share_destination_custom"; private final static String PREFERENCE_DESTINATION = "share_destination_folder_uri"; private static final int RESULT_PICKER = Activity.RESULT_FIRST_USER; private Preference filePicker; + public static ShareSettingsFragment newInstance(@NonNull String pluginKey) { + ShareSettingsFragment fragment = new ShareSettingsFragment(); + fragment.setArguments(pluginKey); + + return fragment; + } + @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + super.onCreatePreferences(savedInstanceState, rootKey); - final CheckBoxPreference customDownloads = (CheckBoxPreference) findPreference("share_destination_custom"); - filePicker = findPreference("share_destination_folder_preference"); + PreferenceScreen preferenceScreen = getPreferenceScreen(); + final CheckBoxPreference customDownloads = (CheckBoxPreference) preferenceScreen.findPreference("share_destination_custom"); + filePicker = preferenceScreen.findPreference("share_destination_folder_preference"); if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)) { customDownloads.setOnPreferenceChangeListener((preference, newValue) -> { @@ -51,13 +81,19 @@ filePicker.setEnabled(false); } - boolean customized = PreferenceManager.getDefaultSharedPreferences(this).getBoolean(PREFERENCE_CUSTOMIZE_DESTINATION, false); + boolean customized = PreferenceManager + .getDefaultSharedPreferences(requireContext()) + .getBoolean(PREFERENCE_CUSTOMIZE_DESTINATION, false); + updateFilePickerStatus(customized); } private void updateFilePickerStatus(boolean enabled) { filePicker.setEnabled(enabled); - String path = PreferenceManager.getDefaultSharedPreferences(this).getString(PREFERENCE_DESTINATION, null); + String path = PreferenceManager + .getDefaultSharedPreferences(requireContext()) + .getString(PREFERENCE_DESTINATION, null); + if (enabled && path != null) { filePicker.setSummary(Uri.parse(path).getPath()); } else { @@ -99,22 +135,20 @@ @TargetApi(Build.VERSION_CODES.KITKAT) @Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { - if (requestCode == RESULT_PICKER && resultCode == Activity.RESULT_OK && resultData != null) { Uri uri = resultData.getData(); - getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | + requireContext().getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); Preference filePicker = findPreference("share_destination_folder_preference"); filePicker.setSummary(uri.getPath()); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()); prefs.edit().putString(PREFERENCE_DESTINATION, uri.toString()).apply(); } } - } diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceSettingsActivity.java b/src/org/kde/kdeconnect/UserInterface/DeviceSettingsActivity.java --- a/src/org/kde/kdeconnect/UserInterface/DeviceSettingsActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/DeviceSettingsActivity.java @@ -21,52 +21,79 @@ package org.kde.kdeconnect.UserInterface; import android.os.Bundle; -import android.preference.PreferenceScreen; import android.view.MenuItem; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; +import org.kde.kdeconnect.Plugins.Plugin; +import org.kde.kdeconnect_tp.R; -import java.util.List; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; -public class DeviceSettingsActivity extends AppCompatPreferenceActivity { +public class DeviceSettingsActivity + extends AppCompatActivity + implements PluginPreference.PluginPreferenceCallback { + public static final String EXTRA_DEVICE_ID = "deviceId"; + public static final String EXTRA_PLUGIN_KEY = "pluginKey"; + + //TODO: Save/restore state 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. @Override public void onCreate(Bundle savedInstanceState) { + ThemeUtil.setUserPreferredTheme(this); super.onCreate(savedInstanceState); - final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(this); - setPreferenceScreen(preferenceScreen); + setContentView(R.layout.activity_device_settings); - if (getIntent().hasExtra("deviceId")) { - deviceId = getIntent().getStringExtra("deviceId"); + if (getSupportActionBar() != null) { + getSupportActionBar().setDefaultDisplayHomeAsUpEnabled(true); } - BackgroundService.RunCommand(getApplicationContext(), service -> { - final Device device = service.getDevice(deviceId); - if (device == null) { - DeviceSettingsActivity.this.runOnUiThread(DeviceSettingsActivity.this::finish); - return; + String pluginKey = null; + + if (getIntent().hasExtra(EXTRA_DEVICE_ID)) { + deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID); + + if (getIntent().hasExtra(EXTRA_PLUGIN_KEY)) { + pluginKey = getIntent().getStringExtra(EXTRA_PLUGIN_KEY); } - List plugins = device.getSupportedPlugins(); - for (final String pluginKey : plugins) { - PluginPreference pref = new PluginPreference(DeviceSettingsActivity.this, pluginKey, device); - preferenceScreen.addPreference(pref); + } else if (deviceId == null) { + throw new RuntimeException("You must start DeviceSettingActivity using an intent that has a " + EXTRA_DEVICE_ID + " extra"); + } + + Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragmentPlaceHolder); + if (fragment == null) { + if (pluginKey == null) { + fragment = DeviceSettingsFragment.newInstance(deviceId); + } else { + Device device = BackgroundService.getInstance().getDevice(deviceId); + Plugin plugin = device.getPlugin(pluginKey, true); + fragment = plugin.getSettingsFragment(); } - }); + + getSupportFragmentManager() + .beginTransaction() + .add(R.id.fragmentPlaceHolder, fragment) + .commit(); + } } @Override public boolean onOptionsItemSelected(MenuItem item) { - //ActionBar's back button if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } else { - return super.onOptionsItemSelected(item); + FragmentManager fm = getSupportFragmentManager(); + + if (fm.getBackStackEntryCount() > 0) { + fm.popBackStack(); + return true; + } } + + return super.onOptionsItemSelected(item); } @Override @@ -81,4 +108,27 @@ BackgroundService.removeGuiInUseCounter(this); } + @Override + public void onStartPluginSettingsFragment(Plugin plugin) { + setTitle(getString(R.string.plugin_settings_with_name, plugin.getDisplayName())); + + PluginSettingsFragment fragment = plugin.getSettingsFragment(); + + //TODO: Remove when NotificationFilterActivity has been turned into a PluginSettingsFragment + if (fragment == null) { + return; + } + + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragmentPlaceHolder, fragment) + .addToBackStack(null) + .commit(); + + } + + @Override + public void onFinish() { + finish(); + } } diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceSettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceSettingsFragment.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/DeviceSettingsFragment.java @@ -0,0 +1,175 @@ +/* + * Copyright 2018 Erik Duisters + * + * 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.os.Bundle; +import android.os.Parcelable; + +import org.kde.kdeconnect.BackgroundService; +import org.kde.kdeconnect.Device; +import org.kde.kdeconnect_tp.R; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceScreen; +import androidx.recyclerview.widget.RecyclerView; + +public class DeviceSettingsFragment extends PreferenceFragmentCompat { + private static final String ARG_DEVICE_ID = "deviceId"; + private static final String KEY_RECYCLERVIEW_LAYOUTMANAGER_STATE = "RecyclerViewLayoutmanagerState"; + + private PluginPreference.PluginPreferenceCallback callback; + private Parcelable recyclerViewLayoutManagerState; + + /* + https://bricolsoftconsulting.com/state-preservation-in-backstack-fragments/ + When adding a fragment to the backstack the fragments onDestroyView is called (which releases + the RecyclerView) but the fragments onSaveInstanceState is not called. When the fragment is destroyed later + on, its onSaveInstanceState() is called but I don't have access to the RecyclerView or it's LayoutManager any more + */ + private boolean stateSaved; + + public static DeviceSettingsFragment newInstance(@NonNull String deviceId) { + DeviceSettingsFragment fragment = new DeviceSettingsFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_DEVICE_ID, deviceId); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if (requireActivity() instanceof PluginPreference.PluginPreferenceCallback) { + callback = (PluginPreference.PluginPreferenceCallback) getActivity(); + } else { + throw new RuntimeException(requireActivity().getClass().getSimpleName() + + " must implement PluginPreference.PluginPreferenceCallback"); + } + + super.onCreate(savedInstanceState); + + if (savedInstanceState != null && savedInstanceState.containsKey(KEY_RECYCLERVIEW_LAYOUTMANAGER_STATE)) { + recyclerViewLayoutManagerState = savedInstanceState.getParcelable(KEY_RECYCLERVIEW_LAYOUTMANAGER_STATE); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + + callback = null; + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(requireContext()); + setPreferenceScreen(preferenceScreen); + + final String deviceId = getArguments().getString(ARG_DEVICE_ID); + + BackgroundService.RunCommand(requireContext(), service -> { + final Device device = service.getDevice(deviceId); + if (device == null) { + final FragmentActivity activity = requireActivity(); + activity.runOnUiThread(() -> activity.finish()); + return; + } + List plugins = device.getSupportedPlugins(); + + for (final String pluginKey : plugins) { + PluginPreference pref = new PluginPreference(requireContext(), pluginKey, device, callback); + preferenceScreen.addPreference(pref); + } + }); + } + + @Override + protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { + RecyclerView.Adapter adapter = super.onCreateAdapter(preferenceScreen); + + /* + The layoutmanager's state (e.g. scroll position) can only be restored when the recyclerView's + adapter has been re-populated with data. + */ + if (recyclerViewLayoutManagerState != null) { + adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { + @Override + public void onChanged() { + RecyclerView.LayoutManager layoutManager = getListView().getLayoutManager(); + + if (layoutManager != null) { + layoutManager.onRestoreInstanceState(recyclerViewLayoutManagerState); + } + + recyclerViewLayoutManagerState = null; + adapter.unregisterAdapterDataObserver(this); + } + }); + } + + return adapter; + } + + @Override + public void onPause() { + super.onPause(); + + stateSaved = false; + } + + @Override + public void onResume() { + super.onResume(); + + requireActivity().setTitle(getString(R.string.device_menu_plugins)); + } + + @Override + public void onDestroyView() { + if (!stateSaved && getListView() != null && getListView().getLayoutManager() != null) { + recyclerViewLayoutManagerState = getListView().getLayoutManager().onSaveInstanceState(); + } + + super.onDestroyView(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + Parcelable layoutManagerState = recyclerViewLayoutManagerState; + + if (getListView() != null && getListView().getLayoutManager() != null) { + layoutManagerState = getListView().getLayoutManager().onSaveInstanceState(); + } + + if (layoutManagerState != null) { + outState.putParcelable(KEY_RECYCLERVIEW_LAYOUTMANAGER_STATE, layoutManagerState); + } + + stateSaved = true; + } +} diff --git a/src/org/kde/kdeconnect/UserInterface/PluginPreference.java b/src/org/kde/kdeconnect/UserInterface/PluginPreference.java --- a/src/org/kde/kdeconnect/UserInterface/PluginPreference.java +++ b/src/org/kde/kdeconnect/UserInterface/PluginPreference.java @@ -1,66 +1,76 @@ package org.kde.kdeconnect.UserInterface; -import android.preference.CheckBoxPreference; +import android.content.Context; import android.view.View; import org.kde.kdeconnect.Device; import org.kde.kdeconnect.Plugins.Plugin; import org.kde.kdeconnect.Plugins.PluginFactory; import org.kde.kdeconnect_tp.R; -class PluginPreference extends CheckBoxPreference { +import androidx.annotation.NonNull; +import androidx.preference.CheckBoxPreference; +import androidx.preference.PreferenceViewHolder; +class PluginPreference extends CheckBoxPreference { private final Device device; private final String pluginKey; private final View.OnClickListener listener; - public PluginPreference(final DeviceSettingsActivity activity, final String pluginKey, final Device device) { - super(activity); + public PluginPreference(@NonNull final Context context, @NonNull final String pluginKey, + @NonNull final Device device, @NonNull PluginPreferenceCallback callback) { + super(context); - setLayoutResource(R.layout.preference_with_button); + setLayoutResource(R.layout.preference_with_button/*R.layout.preference_with_button_androidx*/); this.device = device; this.pluginKey = pluginKey; - PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(activity, pluginKey); + PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(context, pluginKey); setTitle(info.getDisplayName()); setSummary(info.getDescription()); +setIcon(android.R.color.transparent); setChecked(device.isPluginEnabled(pluginKey)); Plugin plugin = device.getPlugin(pluginKey, true); if (info.hasSettings() && plugin != null) { this.listener = v -> { Plugin plugin1 = device.getPlugin(pluginKey, true); if (plugin1 != null) { - plugin1.startPreferencesActivity(activity); + callback.onStartPluginSettingsFragment(plugin1); } else { //Could happen if the device is not connected anymore - activity.finish(); //End this activity so we go to the "device not reachable" screen + callback.onFinish(); } }; } else { this.listener = null; } - } @Override - protected void onBindView(View root) { - super.onBindView(root); - final View button = root.findViewById(R.id.settingsButton); + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + + final View button = holder.findViewById(R.id.settingsButton); if (listener == null) { button.setVisibility(View.GONE); } else { button.setEnabled(isChecked()); + button.setVisibility(View.VISIBLE); button.setOnClickListener(listener); } - root.setOnClickListener(v -> { + holder.itemView.setOnClickListener(v -> { boolean newState = !device.isPluginEnabled(pluginKey); setChecked(newState); //It actually works on API<14 button.setEnabled(newState); device.setPluginEnabled(pluginKey, newState); }); } + interface PluginPreferenceCallback { + void onStartPluginSettingsFragment(Plugin plugin); + void onFinish(); + } } diff --git a/src/org/kde/kdeconnect/UserInterface/PluginSettingsActivity.java b/src/org/kde/kdeconnect/UserInterface/PluginSettingsActivity.java deleted file mode 100644 --- a/src/org/kde/kdeconnect/UserInterface/PluginSettingsActivity.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2014 Ronny Yabar Aizcorbe - * - * 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.os.Bundle; -import android.view.MenuItem; - -import org.kde.kdeconnect.BackgroundService; -import org.kde.kdeconnect_tp.R; - -import java.util.Locale; - -public class PluginSettingsActivity extends AppCompatPreferenceActivity { - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - String pluginDisplayName = getIntent().getStringExtra("plugin_display_name"); - setTitle(getString(R.string.plugin_settings_with_name, pluginDisplayName)); - - String pluginKey = getIntent().getStringExtra("plugin_key"); - int resFile = getResources().getIdentifier(pluginKey.toLowerCase(Locale.ENGLISH) + "_preferences", "xml", getPackageName()); - addPreferencesFromResource(resFile); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - //ActionBar's back button - if (item.getItemId() == android.R.id.home) { - finish(); - return true; - } else { - return super.onOptionsItemSelected(item); - } - } - - @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/PluginSettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/PluginSettingsFragment.java new file mode 100644 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/PluginSettingsFragment.java @@ -0,0 +1,81 @@ +/* + * Copyright 2018 Erik Duisters + * + * 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.os.Bundle; + +import org.kde.kdeconnect.Plugins.PluginFactory; +import org.kde.kdeconnect_tp.R; + +import java.util.Locale; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceFragmentCompat; + +public class PluginSettingsFragment extends PreferenceFragmentCompat { + public static final String ARG_PLUGIN_KEY = "plugin_key"; + + private String pluginKey; + + public static PluginSettingsFragment newInstance(@NonNull String pluginKey) { + PluginSettingsFragment fragment = new PluginSettingsFragment(); + fragment.setArguments(pluginKey); + + return fragment; + } + + public PluginSettingsFragment() {} + + protected Bundle setArguments(@NonNull String pluginKey) { + Bundle args = new Bundle(); + args.putString(ARG_PLUGIN_KEY, pluginKey); + + setArguments(args); + + return args; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + if (getArguments() == null || !getArguments().containsKey(ARG_PLUGIN_KEY)) { + throw new RuntimeException("You must provide a pluginKey by calling setArguments(@NonNull String pluginKey)"); + } + + pluginKey = getArguments().getString(ARG_PLUGIN_KEY); + + super.onCreate(savedInstanceState); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + int resFile = getResources().getIdentifier(pluginKey.toLowerCase(Locale.ENGLISH) + "_preferences", "xml", + requireContext().getPackageName()); + addPreferencesFromResource(resFile); + } + + @Override + public void onResume() { + super.onResume(); + + PluginFactory.PluginInfo info = PluginFactory.getPluginInfo(requireContext(), pluginKey); + requireActivity().setTitle(getString(R.string.plugin_settings_with_name, info.getDisplayName())); + } +}