diff --git a/build.gradle b/build.gradle index f1a99b1c..7e5cb38b 100644 --- a/build.gradle +++ b/build.gradle @@ -72,6 +72,7 @@ dependencies { implementation 'com.android.support:support-media-compat:28.0.0' implementation 'com.android.support:appcompat-v7:28.0.0' + implementation 'com.android.support:preference-v7:28.0.0' implementation 'com.android.support:design:28.0.0' implementation 'com.jakewharton:disklrucache:2.0.2' //For caching album art bitmaps diff --git a/res/values/strings.xml b/res/values/strings.xml index 6f59ac3c..b6c2cf21 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -154,7 +154,7 @@ Device Pair device Remote control - KDE Connect Settings + Settings Play Pause Previous @@ -263,7 +263,7 @@ Fullscreen Exit presentation You can lock your device to use the volume keys as previous/next buttons - + Add a command There are no commands registered You can add new commands in the KDE Connect System Settings @@ -292,5 +292,10 @@ System volume Control the system volume of the remote device Mute + Devices + + Dark mode + More settings + Per-device settings can be found under \'Plugin settings\' from within a device. diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index 540cf248..2cc064cd 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -7,6 +7,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; @@ -25,11 +26,13 @@ import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.widget.CompoundButton; import android.widget.EditText; import android.widget.TextView; +import android.widget.Toast; import org.kde.kdeconnect.BackgroundService; import org.kde.kdeconnect.Device; @@ -38,10 +41,19 @@ import org.kde.kdeconnect_tp.R; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.Attributes; public class MainActivity extends AppCompatActivity { - private static final String STATE_SELECTED_DEVICE = "selected_device"; + private static final int MENU_ENTRY_ADD_DEVICE = 1; //0 means no-selection + private static final int MENU_ENTRY_SETTINGS = 2; + private static final int MENU_ENTRY_DEVICE_FIRST_ID = 1000; //All subsequent ids are devices in the menu + private static final int MENU_ENTRY_DEVICE_UNKNOWN = 9999; //It's still a device, but we don't know which one yet + + private static final String STATE_SELECTED_MENU_ENTRY = "selected_entry"; //Saved only in onSaveInstanceState + private static final String STATE_SELECTED_DEVICE = "selected_device"; //Saved persistently in preferences public static final int RESULT_NEEDS_RELOAD = Activity.RESULT_FIRST_USER; @@ -53,10 +65,12 @@ public class MainActivity extends AppCompatActivity { private DrawerLayout mDrawerLayout; private String mCurrentDevice; + private int mCurrentMenuEntry; private SharedPreferences preferences; private final HashMap mMapMenuToDeviceId = new HashMap<>(); + private String pairRequestStatus; @Override protected void onCreate(Bundle savedInstanceState) { @@ -66,11 +80,11 @@ public class MainActivity extends AppCompatActivity { ThemeUtil.setUserPreferredTheme(this); setContentView(R.layout.activity_main); - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - mNavigationView = (NavigationView) findViewById(R.id.navigation_drawer); + mDrawerLayout = findViewById(R.id.drawer_layout); + mNavigationView = findViewById(R.id.navigation_drawer); View mDrawerHeader = mNavigationView.getHeaderView(0); - Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); + Toolbar toolbar = findViewById(R.id.toolbar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); @@ -89,104 +103,111 @@ public class MainActivity extends AppCompatActivity { mDrawerToggle.syncState(); String deviceName = DeviceHelper.getDeviceName(this); - TextView nameView = (TextView) mDrawerHeader.findViewById(R.id.device_name); + TextView nameView = mDrawerHeader.findViewById(R.id.device_name); nameView.setText(deviceName); - View.OnClickListener renameListener = v -> renameDevice(); + View.OnClickListener renameListener = v -> openRenameDeviceDialog(); mDrawerHeader.findViewById(R.id.kdeconnect_label).setOnClickListener(renameListener); mDrawerHeader.findViewById(R.id.device_name).setOnClickListener(renameListener); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { - addDarkModeSwitch((ViewGroup) mDrawerHeader); - } + preferences = getSharedPreferences("stored_menu_selection", Context.MODE_PRIVATE); mNavigationView.setNavigationItemSelectedListener(menuItem -> { - - String deviceId = mMapMenuToDeviceId.get(menuItem); - onDeviceSelected(deviceId); + mCurrentMenuEntry = menuItem.getItemId(); + switch (mCurrentMenuEntry) { + case MENU_ENTRY_ADD_DEVICE: + mCurrentDevice = null; + preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply(); + setContentFragment(new PairingFragment()); + break; + case MENU_ENTRY_SETTINGS: + mCurrentDevice = null; + preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply(); + setContentFragment(new SettingsFragment()); + break; + default: + assert(mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID); + String deviceId = mMapMenuToDeviceId.get(menuItem); + onDeviceSelected(deviceId); + break; + } mDrawerLayout.closeDrawer(mNavigationView); - return true; }); - preferences = getSharedPreferences(STATE_SELECTED_DEVICE, Context.MODE_PRIVATE); + // Decide which menu entry should be selected at start String savedDevice; - String pairStatus = ""; + int savedMenuEntry; if (getIntent().hasExtra("forceOverview")) { Log.i("MainActivity", "Requested to start main overview"); savedDevice = null; + savedMenuEntry = MENU_ENTRY_ADD_DEVICE; } else if (getIntent().hasExtra("deviceId")) { Log.i("MainActivity", "Loading selected device from parameter"); savedDevice = getIntent().getStringExtra("deviceId"); - if (getIntent().hasExtra(PAIR_REQUEST_STATUS)) { - pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS); + savedMenuEntry = MENU_ENTRY_DEVICE_UNKNOWN; + // If pairStatus is not empty, then the user has accepted/reject the pairing from the notification + String pairStatus = getIntent().getStringExtra(PAIR_REQUEST_STATUS); + if (pairStatus != null) { + Log.i("MainActivity", "pair status is " + pairStatus); + onPairResultFromNotification(savedDevice, pairStatus); } } else if (savedInstanceState != null) { Log.i("MainActivity", "Loading selected device from saved activity state"); savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); + savedMenuEntry = savedInstanceState.getInt(STATE_SELECTED_MENU_ENTRY, MENU_ENTRY_ADD_DEVICE); } else { Log.i("MainActivity", "Loading selected device from persistent storage"); savedDevice = preferences.getString(STATE_SELECTED_DEVICE, null); + savedMenuEntry = (savedDevice != null)? MENU_ENTRY_DEVICE_UNKNOWN : MENU_ENTRY_ADD_DEVICE; } - //if pairStatus is not empty, then the decision has been made... - if (!pairStatus.equals("")) { - Log.i("MainActivity", "pair status is " + pairStatus); - onNewDeviceSelected(savedDevice, pairStatus); - } - onDeviceSelected(savedDevice); - } - /** - * Adds a {@link SwitchCompat} to the bottom of the navigation header for - * toggling dark mode on and off. Call from {@link #onCreate(Bundle)}. - * - * Only supports android ICS and higher because {@link SwitchCompat} - * requires that. - * - * - * @param drawerHeader the layout which should contain the switch - */ - @RequiresApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - private void addDarkModeSwitch(ViewGroup drawerHeader) { - getLayoutInflater().inflate(R.layout.nav_dark_mode_switch, drawerHeader); - SwitchCompat darkThemeSwitch = (SwitchCompat) drawerHeader.findViewById(R.id.dark_theme); - darkThemeSwitch.setChecked(ThemeUtil.shouldUseDarkTheme(this)); - darkThemeSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @RequiresApi(Build.VERSION_CODES.HONEYCOMB) - @Override - public void onCheckedChanged(CompoundButton darkThemeSwitch, boolean isChecked) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(MainActivity.this); - boolean isDarkAlready = prefs.getBoolean("darkTheme", false); - if (isDarkAlready != isChecked) { - prefs.edit().putBoolean("darkTheme", isChecked).apply(); - MainActivity.this.recreate(); - } + // Activate the chosen fragment and select the entry in the menu + + if (savedMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID && savedDevice != null) { + onDeviceSelected(savedDevice); + } else { + mCurrentMenuEntry = savedMenuEntry; + mNavigationView.setCheckedItem(savedMenuEntry); + if (mCurrentMenuEntry == MENU_ENTRY_SETTINGS) { + setContentFragment(new SettingsFragment()); + } else { + setContentFragment(new PairingFragment()); } - }); + } } - //like onNewDeviceSelected but assumes that the new device is simply requesting to be paired - //and can't be null - private void onNewDeviceSelected(String deviceId, String pairStatus) { + + private void onPairResultFromNotification(String deviceId, String pairStatus) { + + assert(deviceId != null); + mCurrentDevice = deviceId; - preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply(); + preferences.edit().putString(STATE_SELECTED_DEVICE, null).apply(); - for (HashMap.Entry entry : mMapMenuToDeviceId.entrySet()) { - boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe - entry.getKey().setChecked(selected); - } + mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice); + mNavigationView.setCheckedItem(mCurrentMenuEntry); if (pairStatus.equals(PAIRING_ACCEPTED)) { - DeviceFragment.acceptPairing(deviceId, this); + DeviceFragment.acceptPairing(mCurrentDevice, this); } else { - DeviceFragment.rejectPairing(deviceId, this); + DeviceFragment.rejectPairing(mCurrentDevice, this); } } + private int deviceIdToMenuEntryId(String deviceId) { + for (HashMap.Entry 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)) { @@ -206,43 +227,50 @@ public class MainActivity extends AppCompatActivity { } } - private void updateComputerList() { - - //Log.e("MainActivity", "UpdateComputerList"); + private void updateDeviceList() { BackgroundService.RunCommand(MainActivity.this, service -> { Menu menu = mNavigationView.getMenu(); - menu.clear(); mMapMenuToDeviceId.clear(); - int id = 0; + SubMenu devicesMenu = menu.addSubMenu(R.string.devices); + + int id = MENU_ENTRY_DEVICE_FIRST_ID; Collection devices = service.getDevices().values(); for (Device device : devices) { if (device.isReachable() && device.isPaired()) { - MenuItem item = menu.add(0, id++, 0, device.getName()); + MenuItem item = devicesMenu.add(Menu.FIRST, id++, 1, device.getName()); item.setIcon(device.getIcon()); item.setCheckable(true); - item.setChecked(device.getDeviceId().equals(mCurrentDevice)); mMapMenuToDeviceId.put(item, device.getDeviceId()); } } - MenuItem item = menu.add(99, id++, 0, R.string.pair_new_device); - item.setIcon(R.drawable.ic_action_content_add_circle_outline); - item.setCheckable(true); - item.setChecked(mCurrentDevice == null); - mMapMenuToDeviceId.put(item, null); + MenuItem addDeviceItem = devicesMenu.add(Menu.FIRST, MENU_ENTRY_ADD_DEVICE, 1000, R.string.pair_new_device); + addDeviceItem.setIcon(R.drawable.ic_action_content_add_circle_outline); + addDeviceItem.setCheckable(true); + + MenuItem settingsItem = menu.add(Menu.FIRST, MENU_ENTRY_SETTINGS, 1000, R.string.settings); + settingsItem.setIcon(R.drawable.ic_action_settings); + settingsItem.setCheckable(true); + + //Ids might have changed + if (mCurrentMenuEntry >= MENU_ENTRY_DEVICE_FIRST_ID) { + mCurrentMenuEntry = deviceIdToMenuEntryId(mCurrentDevice); + } + mNavigationView.setCheckedItem(mCurrentMenuEntry); }); } + @Override protected void onStart() { super.onStart(); BackgroundService.addGuiInUseCounter(this, true); - BackgroundService.RunCommand(this, service -> service.addDeviceListChangedCallback("MainActivity", this::updateComputerList)); - updateComputerList(); + BackgroundService.RunCommand(this, service -> service.addDeviceListChangedCallback("MainActivity", this::updateDeviceList)); + updateDeviceList(); } @Override @@ -252,27 +280,38 @@ public class MainActivity extends AppCompatActivity { super.onStop(); } - //TODO: Make it accept two parameters, a constant with the type of screen and the device id in - //case the screen is for a device, or even three parameters and the third one be the plugin id? - //This way we can keep adding more options with null device id (eg: about, help...) + private static void uncheckAllMenuItems(Menu menu) { + int size = menu.size(); + for (int i = 0; i < size; i++) { + MenuItem item = menu.getItem(i); + if(item.hasSubMenu()) { + uncheckAllMenuItems(item.getSubMenu()); + } else { + item.setChecked(false); + } + } + } + public void onDeviceSelected(String deviceId, boolean fromDeviceList) { - mCurrentDevice = deviceId; + preferences.edit().putString(STATE_SELECTED_DEVICE, deviceId).apply(); - preferences.edit().putString(STATE_SELECTED_DEVICE, mCurrentDevice).apply(); - - for (HashMap.Entry entry : mMapMenuToDeviceId.entrySet()) { - boolean selected = TextUtils.equals(entry.getValue(), deviceId); //null-safe - entry.getKey().setChecked(selected); - } - - Fragment fragment; - if (deviceId == null) { - fragment = new PairingFragment(); + if (mCurrentDevice != null) { + mCurrentMenuEntry = deviceIdToMenuEntryId(deviceId); + if (mCurrentMenuEntry == MENU_ENTRY_DEVICE_UNKNOWN) { + uncheckAllMenuItems(mNavigationView.getMenu()); + } else { + mNavigationView.setCheckedItem(mCurrentMenuEntry); + } + setContentFragment(new DeviceFragment(deviceId, fromDeviceList)); } else { - fragment = new DeviceFragment(deviceId, fromDeviceList); + mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE; + mNavigationView.setCheckedItem(mCurrentMenuEntry); + setContentFragment(new PairingFragment()); } + } + private void setContentFragment(Fragment fragment) { getSupportFragmentManager() .beginTransaction() .replace(R.id.container, fragment) @@ -287,13 +326,7 @@ public class MainActivity extends AppCompatActivity { protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putString(STATE_SELECTED_DEVICE, mCurrentDevice); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - String savedDevice = savedInstanceState.getString(STATE_SELECTED_DEVICE); - onDeviceSelected(savedDevice); + outState.putInt(STATE_SELECTED_MENU_ENTRY, mCurrentMenuEntry); } @Override @@ -323,28 +356,45 @@ public class MainActivity extends AppCompatActivity { } } - public void renameDevice() { - final TextView nameView = (TextView) mNavigationView.findViewById(R.id.device_name); - final EditText deviceNameEdit = new EditText(MainActivity.this); - String deviceName = DeviceHelper.getDeviceName(MainActivity.this); + + interface NameChangeCallback { + void onNameChanged(String newName); + } + + private Set nameChangeSubscribers = new HashSet<>(); + + public void addNameChangeCallback(NameChangeCallback cb) { + nameChangeSubscribers.add(cb); + } + + public void removeNameChangeCallback(NameChangeCallback cb) { + nameChangeSubscribers.remove(cb); + } + + public void openRenameDeviceDialog() { + final EditText deviceNameEdit = new EditText(this); + String deviceName = DeviceHelper.getDeviceName(this); deviceNameEdit.setText(deviceName); - deviceNameEdit.setPadding( - ((int) (18 * getResources().getDisplayMetrics().density)), - ((int) (16 * getResources().getDisplayMetrics().density)), - ((int) (18 * getResources().getDisplayMetrics().density)), - ((int) (12 * getResources().getDisplayMetrics().density)) - ); - new AlertDialog.Builder(MainActivity.this) + float dpi = this.getResources().getDisplayMetrics().density; + deviceNameEdit.setPadding( ((int) (18 * dpi)), ((int) (16 * dpi)), ((int) (18 * dpi)), ((int) (12 * dpi)) ); + new AlertDialog.Builder(this) .setView(deviceNameEdit) .setPositiveButton(R.string.device_rename_confirm, (dialog, which) -> { - String deviceName1 = deviceNameEdit.getText().toString(); - DeviceHelper.setDeviceName(MainActivity.this, deviceName1); - nameView.setText(deviceName1); - BackgroundService.RunCommand(MainActivity.this, BackgroundService::onNetworkChange); - }) - .setNegativeButton(R.string.cancel, (dialog, which) -> { + String newDeviceName = deviceNameEdit.getText().toString(); + DeviceHelper.setDeviceName(this, newDeviceName); + this.updateDeviceNameFromMenu(newDeviceName); + BackgroundService.RunCommand(this, BackgroundService::onNetworkChange); + for (NameChangeCallback callback : nameChangeSubscribers) { + callback.onNameChanged(newDeviceName); + } }) + .setNegativeButton(R.string.cancel, (dialog, which) -> { }) .setTitle(R.string.device_rename_title) .show(); } + + public void updateDeviceNameFromMenu(String newDeviceName) { + final TextView nameView = mNavigationView.findViewById(R.id.device_name); + nameView.setText(newDeviceName); + } } diff --git a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java index 60c3d6f6..d95e4bbd 100644 --- a/src/org/kde/kdeconnect/UserInterface/PairingFragment.java +++ b/src/org/kde/kdeconnect/UserInterface/PairingFragment.java @@ -238,7 +238,7 @@ public class PairingFragment extends Fragment implements PairingDeviceItem.Callb updateComputerListAction(); break; case R.id.menu_rename: - mActivity.renameDevice(); + mActivity.openRenameDeviceDialog(); break; case R.id.menu_custom_device_list: startActivity(new Intent(mActivity, CustomDevicesActivity.class)); diff --git a/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java new file mode 100644 index 00000000..6131c477 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/SettingsFragment.java @@ -0,0 +1,104 @@ +package org.kde.kdeconnect.UserInterface; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.support.v14.preference.SwitchPreference; +import android.support.v7.preference.Preference; +import android.support.v7.preference.PreferenceFragmentCompat; +import android.support.v7.preference.PreferenceScreen; +import android.util.Log; + +import org.kde.kdeconnect.Helpers.DeviceHelper; +import org.kde.kdeconnect_tp.R; + +public class SettingsFragment extends PreferenceFragmentCompat implements MainActivity.NameChangeCallback { + + MainActivity mainActivity; + private Preference renameDevice; + + @Override + public void onDestroy() { + mainActivity.removeNameChangeCallback(this); + super.onDestroy(); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + + mainActivity = (MainActivity)getActivity(); + Context context = mainActivity; + + PreferenceScreen screen = getPreferenceManager().createPreferenceScreen(context); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Rename device + mainActivity.addNameChangeCallback(this); + renameDevice = new Preference(context); + renameDevice.setPersistent(false); + renameDevice.setSelectable(true); + renameDevice.setOnPreferenceClickListener(preference -> { + mainActivity.openRenameDeviceDialog(); + return true; + }); + String deviceName = DeviceHelper.getDeviceName(context); + renameDevice.setTitle(R.string.device_rename_title); + renameDevice.setSummary(deviceName); + screen.addPreference(renameDevice); + + + //TODO: Trusted wifi networks settings should go here + + + // Dark mode + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + final SwitchPreference darkThemeSwitch = new SwitchPreference(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); + } + + + //TODO: Persistent notification toggle for pre-oreo? + + + // 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 onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + Log.e("AAAAAAAAAA","CHANGEEEED"); + } + + + @Override + public void onNameChanged(String newName) { + renameDevice.setSummary(newName); + } +}
- * Only supports android ICS and higher because {@link SwitchCompat} - * requires that. - *