From 0ed11d2036d68dfca59c78d42caff6af69eaa6c0 Mon Sep 17 00:00:00 2001 From: Dmitry Yudin Date: Wed, 29 Mar 2023 18:22:27 +0000 Subject: [PATCH] Show enabled plugins as cards --- res/layout/activity_device.xml | 64 +-- res/layout/devices_list.xml | 6 +- res/layout/list_plugin_entry.xml | 43 ++ res/values-land/consts.xml | 9 + res/values-w820dp/consts.xml | 6 + res/values/consts.xml | 14 + res/values/strings.xml | 1 + .../UserInterface/DeviceFragment.java | 446 ------------------ .../UserInterface/DeviceFragment.kt | 398 ++++++++++++++++ .../UserInterface/List/EntryItemWithIcon.java | 15 +- .../UserInterface/MainActivity.java | 2 +- 11 files changed, 498 insertions(+), 506 deletions(-) create mode 100644 res/layout/list_plugin_entry.xml create mode 100644 res/values-land/consts.xml create mode 100644 res/values-w820dp/consts.xml create mode 100644 res/values/consts.xml delete mode 100644 src/org/kde/kdeconnect/UserInterface/DeviceFragment.java create mode 100644 src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt diff --git a/res/layout/activity_device.xml b/res/layout/activity_device.xml index a0505c05..97e04aaf 100644 --- a/res/layout/activity_device.xml +++ b/res/layout/activity_device.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical" + android:orientation="@integer/activity_device_orientation" tools:context="org.kde.kdeconnect.UserInterface.DeviceFragment"> @@ -18,51 +18,27 @@ layout="@layout/view_pair_error" tools:visibility="gone"/> - + + + - - - - - - - - - - + tools:layout_height="300dp" /> + \ No newline at end of file diff --git a/res/layout/devices_list.xml b/res/layout/devices_list.xml index d9b6ddf5..2f15f564 100644 --- a/res/layout/devices_list.xml +++ b/res/layout/devices_list.xml @@ -3,7 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" android:id="@+id/refresh_list_layout" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:layout_margin="@dimen/activity_horizontal_margin"> \ No newline at end of file diff --git a/res/layout/list_plugin_entry.xml b/res/layout/list_plugin_entry.xml new file mode 100644 index 00000000..c6362b61 --- /dev/null +++ b/res/layout/list_plugin_entry.xml @@ -0,0 +1,43 @@ + + + + + + + + + + \ No newline at end of file diff --git a/res/values-land/consts.xml b/res/values-land/consts.xml new file mode 100644 index 00000000..4a50ba8d --- /dev/null +++ b/res/values-land/consts.xml @@ -0,0 +1,9 @@ + + + + @integer/orientation_horizontal + + 4 + 6 + + \ No newline at end of file diff --git a/res/values-w820dp/consts.xml b/res/values-w820dp/consts.xml new file mode 100644 index 00000000..d41f57c1 --- /dev/null +++ b/res/values-w820dp/consts.xml @@ -0,0 +1,6 @@ + + + + 3 + + \ No newline at end of file diff --git a/res/values/consts.xml b/res/values/consts.xml new file mode 100644 index 00000000..bcf1ec5f --- /dev/null +++ b/res/values/consts.xml @@ -0,0 +1,14 @@ + + + + + 0 + 1 + + + @integer/orientation_vertical + @null + @null + 2 + + \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 3fd701ea..0f5998d5 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -315,6 +315,7 @@ Some Plugins need permissions to work (tap for more info): This plugin needs permissions to work + All permissions granted 🎉 You need to grant extra permissions to enable all functions Some plugins have features disabled because of lack of permission (tap for more info): To receive files you need to allow storage access diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java deleted file mode 100644 index 3d454fa2..00000000 --- a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora - * - * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL - */ - -package org.kde.kdeconnect.UserInterface; - -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 androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; -import androidx.fragment.app.Fragment; - -import org.kde.kdeconnect.BackgroundService; -import org.kde.kdeconnect.Device; -import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper; -import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin; -import org.kde.kdeconnect.Plugins.BatteryPlugin.DeviceBatteryInfo; -import org.kde.kdeconnect.Plugins.Plugin; -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.PluginListHeaderItem; -import org.kde.kdeconnect_tp.R; -import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding; -import org.kde.kdeconnect_tp.databinding.ViewPairErrorBinding; -import org.kde.kdeconnect_tp.databinding.ViewPairRequestBinding; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.ConcurrentModificationException; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * 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 static final String TAG = "KDE/DeviceFragment"; - - private String mDeviceId; - private Device device; - - private MainActivity mActivity; - - private ArrayList pluginListItems; - - /** - * Top-level ViewBinding for this fragment. - * - * Host for {@link #pluginListItems}. - */ - private ActivityDeviceBinding deviceBinding; - /** - * Not-yet-paired ViewBinding. - * - * Used to start and retry pairing. - */ - private ViewPairRequestBinding binding; - /** - * Cannot-communicate ViewBinding. - * - * Used when the remote device is unreachable. - */ - private ViewPairErrorBinding errorBinding; - - 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(@NonNull Context context) { - super.onAttach(context); - 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(@NonNull LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - deviceBinding = ActivityDeviceBinding.inflate(inflater, container, false); - - // Inner binding for the layout shown when we're not paired yet... - binding = deviceBinding.pairRequest; - // ...and for when pairing doesn't (or can't) work - errorBinding = deviceBinding.pairError; - - binding.pairButton.setOnClickListener(v -> { - binding.pairButton.setVisibility(View.GONE); - binding.pairMessage.setText(""); - binding.pairVerification.setVisibility(View.VISIBLE); - binding.pairVerification.setText(SslHelper.getVerificationKey(SslHelper.certificate, device.certificate)); - binding.pairProgress.setVisibility(View.VISIBLE); - BackgroundService.RunCommand(mActivity, service -> { - device = service.getDevice(mDeviceId); - if (device == null) return; - device.requestPairing(); - }); - }); - binding.acceptButton.setOnClickListener(v -> { - if (device != null) { - device.acceptPairing(); - binding.pairingButtons.setVisibility(View.GONE); - } - }); - binding.rejectButton.setOnClickListener(v -> { - 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); - }); - - setHasOptionsMenu(true); - - BackgroundService.RunCommand(mActivity, service -> { - device = service.getDevice(mDeviceId); - if (device == null) { - Log.e(TAG, "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 deviceBinding.getRoot(); - } - - String getDeviceId() { return mDeviceId; } - - private final Device.PluginsChangedListener pluginsChangedListener = device -> refreshUI(); - - @Override - public void onDestroyView() { - BackgroundService.RunCommand(mActivity, service -> { - Device device = service.getDevice(mDeviceId); - if (device == null) return; - device.removePluginsChangedListener(pluginsChangedListener); - device.removePairingCallback(pairingCallback); - }); - - super.onDestroyView(); - binding = null; - } - - @Override - public void onPrepareOptionsMenu(@NonNull Menu menu) { - 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, PluginSettingsActivity.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.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() { - if (device != null) { - //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 == null || binding == null) { - return; - } - if (device.isPairRequestedByPeer()) { - binding.pairMessage.setText(R.string.pair_requested); - binding.pairVerification.setVisibility(View.VISIBLE); - binding.pairVerification.setText(SslHelper.getVerificationKey(SslHelper.certificate, device.certificate)); - binding.pairingButtons.setVisibility(View.VISIBLE); - binding.pairProgress.setVisibility(View.GONE); - binding.pairButton.setVisibility(View.GONE); - binding.pairRequestButtons.setVisibility(View.VISIBLE); - } else { - boolean paired = device.isPaired(); - boolean reachable = device.isReachable(); - - binding.pairingButtons.setVisibility(paired ? View.GONE : View.VISIBLE); - errorBinding.errorMessageContainer.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); - errorBinding.notReachableMessage.setVisibility((paired && !reachable) ? View.VISIBLE : View.GONE); - deviceBinding.viewStatusContainer.setVisibility((paired && reachable) ? 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(getContext())) continue; - if (p.displayInContextMenu()) continue; - - pluginListItems.add(new PluginItem(p, v -> p.startMainActivity(mActivity))); - } - DeviceFragment.this.createPluginsList(device.getPluginsWithoutPermissions(), R.string.plugins_need_permission, (plugin) -> { - DialogFragment dialog = plugin.getPermissionExplanationDialog(); - if (dialog != null) { - dialog.show(getChildFragmentManager(), null); - } - }); - DeviceFragment.this.createPluginsList(device.getPluginsWithoutOptionalPermissions(), R.string.plugins_need_optional_permission, (plugin) -> { - DialogFragment dialog = plugin.getOptionalPermissionExplanationDialog(); - - if (dialog != null) { - dialog.show(getChildFragmentManager(), null); - } - }); - - DeviceFragment.this.displayBatteryInfoIfPossible(); - } - - ListAdapter adapter = new ListAdapter(mActivity, pluginListItems); - deviceBinding.buttonsList.setAdapter(adapter); - - mActivity.invalidateOptionsMenu(); - - } catch (IllegalStateException e) { - //Ignore: The activity was closed while we were trying to update it - } catch (ConcurrentModificationException e) { - Log.e(TAG, "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 (binding == null) return; - binding.pairMessage.setText(error); - binding.pairVerification.setText(""); - binding.pairVerification.setVisibility(View.GONE); - binding.pairProgress.setVisibility(View.GONE); - binding.pairButton.setVisibility(View.VISIBLE); - binding.pairRequestButtons.setVisibility(View.GONE); - refreshUI(); - }); - } - - @Override - public void unpaired() { - mActivity.runOnUiThread(() -> { - if (binding == null) return; - binding.pairMessage.setText(R.string.device_not_paired); - binding.pairVerification.setVisibility(View.GONE); - binding.pairProgress.setVisibility(View.GONE); - binding.pairButton.setVisibility(View.VISIBLE); - binding.pairRequestButtons.setVisibility(View.GONE); - refreshUI(); - }); - } - - }; - - private void createPluginsList(ConcurrentHashMap plugins, int headerText, FailedPluginListItem.Action action) { - if (plugins.isEmpty()) - return; - - pluginListItems.add(new PluginListHeaderItem(headerText)); - for (Plugin plugin : plugins.values()) { - if (!device.isPluginEnabled(plugin.getPluginKey())) { - continue; - } - pluginListItems.add(new FailedPluginListItem(plugin, action)); - } - } - - /** - * This method tries to display battery info for the remote device. Includes - *
    - *
  • Current charge as a percentage
  • - *
  • Whether the remote device is low on power
  • - *
  • Whether the remote device is currently charging
  • - *
- *

- * This will show a simple message on the view instead if we don't have - * accurate info right now. - *

- */ - private void displayBatteryInfoIfPossible() { - boolean canDisplayBatteryInfo = false; - BatteryPlugin batteryPlugin = (BatteryPlugin) device.getLoadedPlugins().get( - Plugin.getPluginKey(BatteryPlugin.class) - ); - - if (batteryPlugin != null) { - DeviceBatteryInfo info = batteryPlugin.getRemoteBatteryInfo(); - if (info != null) { - canDisplayBatteryInfo = true; - - Context ctx = deviceBinding.viewBatteryStatus.getContext(); - - boolean isCharging = info.isCharging(); - @StringRes - int resId; - if (isCharging) { - resId = R.string.battery_status_charging_format; - } else if (BatteryPlugin.isLowBattery(info)) { - resId = R.string.battery_status_low_format; - } else { - resId = R.string.battery_status_format; - } - - deviceBinding.viewBatteryStatus.setChecked(isCharging); - deviceBinding.viewBatteryStatus.setText(ctx.getString(resId, info.getCurrentCharge())); - } - } - - if (!canDisplayBatteryInfo) { - deviceBinding.viewBatteryStatus.setText(R.string.battery_status_unknown); - } - } -} diff --git a/src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt new file mode 100644 index 00000000..f0c389a8 --- /dev/null +++ b/src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt @@ -0,0 +1,398 @@ +/* + * SPDX-FileCopyrightText: 2014 Albert Vaca Cintora + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +package org.kde.kdeconnect.UserInterface + +import android.content.Intent +import android.content.res.Configuration +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.LinearLayout +import androidx.annotation.StringRes +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.kde.kdeconnect.BackgroundService +import org.kde.kdeconnect.Device +import org.kde.kdeconnect.Device.PairingCallback +import org.kde.kdeconnect.Device.PluginsChangedListener +import org.kde.kdeconnect.Helpers.SecurityHelpers.SslHelper +import org.kde.kdeconnect.Plugins.BatteryPlugin.BatteryPlugin +import org.kde.kdeconnect.Plugins.Plugin +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.PluginListHeaderItem +import org.kde.kdeconnect_tp.R +import org.kde.kdeconnect_tp.databinding.ActivityDeviceBinding +import org.kde.kdeconnect_tp.databinding.ViewPairErrorBinding +import org.kde.kdeconnect_tp.databinding.ViewPairRequestBinding +import java.util.concurrent.ConcurrentHashMap + +/** + * Main view. Displays the current device and its plugins + */ +class DeviceFragment : Fragment() { + val deviceId: String by lazy { + arguments?.getString(ARG_DEVICE_ID) + ?: throw RuntimeException("You must instantiate a new DeviceFragment using DeviceFragment.newInstance()") + } + private var device: Device? = null + private val mActivity: MainActivity? by lazy { activity as MainActivity? } + + //TODO use LinkedHashMap and delete irrelevant records when plugins changed + private val pluginListItems: ArrayList = ArrayList() + private val permissionListItems: ArrayList = ArrayList() + + /** + * Top-level ViewBinding for this fragment. + * + * Host for [.pluginListItems]. + */ + private var deviceBinding: ActivityDeviceBinding? = null + private fun requireDeviceBinding() = deviceBinding ?: throw IllegalStateException("deviceBinding is not set") + + /** + * Not-yet-paired ViewBinding. + * + * Used to start and retry pairing. + */ + private var binding: ViewPairRequestBinding? = null + private fun requireBinding() = binding ?: throw IllegalStateException("binding is not set") + + /** + * Cannot-communicate ViewBinding. + * + * Used when the remote device is unreachable. + */ + private var errorBinding: ViewPairErrorBinding? = null + private fun requireErrorBinding() = errorBinding ?: throw IllegalStateException("errorBinding is not set") + + companion object { + private const val ARG_DEVICE_ID = "deviceId" + private const val ARG_FROM_DEVICE_LIST = "fromDeviceList" + private const val TAG = "KDE/DeviceFragment" + fun newInstance(deviceId: String?, fromDeviceList: Boolean): DeviceFragment { + val frag = DeviceFragment() + val args = Bundle() + args.putString(ARG_DEVICE_ID, deviceId) + args.putBoolean(ARG_FROM_DEVICE_LIST, fromDeviceList) + frag.arguments = args + return frag + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + deviceBinding = ActivityDeviceBinding.inflate(inflater, container, false) + val deviceBinding = deviceBinding ?: return null + + // Inner binding for the layout shown when we're not paired yet... + binding = deviceBinding.pairRequest + // ...and for when pairing doesn't (or can't) work + errorBinding = deviceBinding.pairError + + BackgroundService.RunCommand(mActivity) { + device = it.getDevice(deviceId) + } + + requireBinding().pairButton.setOnClickListener { + requireBinding().pairButton.visibility = View.GONE + requireBinding().pairMessage.text = null + requireBinding().pairVerification.visibility = View.VISIBLE + requireBinding().pairVerification.text = SslHelper.getVerificationKey(SslHelper.certificate, device?.certificate) + requireBinding().pairProgress.visibility = View.VISIBLE + device?.requestPairing() + } + requireBinding().acceptButton.setOnClickListener { + device?.apply { + acceptPairing() + requireBinding().pairingButtons.visibility = View.GONE + } + } + requireBinding().rejectButton.setOnClickListener { + device?.apply { + //Remove listener so buttons don't show for a while before changing the view + removePluginsChangedListener(pluginsChangedListener) + removePairingCallback(pairingCallback) + rejectPairing() + } + mActivity?.onDeviceSelected(null) + } + setHasOptionsMenu(true) + BackgroundService.RunCommand(mActivity) { service: BackgroundService -> + device = service.getDevice(deviceId) ?: let { + Log.e(TAG, "Trying to display a device fragment but the device is not present") + mActivity?.onDeviceSelected(null) + return@RunCommand + } + mActivity?.supportActionBar?.title = device?.name + device?.addPairingCallback(pairingCallback) + device?.addPluginsChangedListener(pluginsChangedListener) + refreshUI() + } + + return deviceBinding.root + } + + private val pluginsChangedListener = PluginsChangedListener { refreshUI() } + override fun onDestroyView() { + BackgroundService.RunCommand(mActivity) { service: BackgroundService -> + val device = service.getDevice(deviceId) ?: return@RunCommand + device.removePluginsChangedListener(pluginsChangedListener) + device.removePairingCallback(pairingCallback) + } + super.onDestroyView() + binding = null + errorBinding = null + deviceBinding = null + } + + override fun onPrepareOptionsMenu(menu: Menu) { + super.onPrepareOptionsMenu(menu) + menu.clear() + val device = device ?: return + + //Plugins button list + val plugins: Collection = device.loadedPlugins.values + for (p in plugins) { + if (!p.displayInContextMenu()) { + continue + } + menu.add(p.actionName).setOnMenuItemClickListener { + p.startMainActivity(mActivity) + true + } + } + val intent = Intent(mActivity, PluginSettingsActivity::class.java) + intent.putExtra("deviceId", deviceId) + menu.add(R.string.device_menu_plugins).setOnMenuItemClickListener { + startActivity(intent) + true + } + if (device.isReachable) { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(requireContext().resources.getString(R.string.encryption_info_title)) + builder.setPositiveButton(requireContext().resources.getString(R.string.ok)) { dialog, _ -> + dialog.dismiss() + } + if (device.certificate == null) { + builder.setMessage(R.string.encryption_info_msg_no_ssl) + } else { + builder.setMessage( + "${ + requireContext().resources.getString(R.string.my_device_fingerprint) + } \n ${ + SslHelper.getCertificateHash(SslHelper.certificate) + } \n\n ${ + requireContext().resources.getString(R.string.remote_device_fingerprint) + } \n ${ + SslHelper.getCertificateHash(device.certificate) + }" + ) + } + menu.add(R.string.encryption_info_title).setOnMenuItemClickListener { + builder.show() + true + } + } + if (device.isPaired) { + menu.add(R.string.device_menu_unpair).setOnMenuItemClickListener { + //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) + true + } + } + } + + override fun onResume() { + super.onResume() + requireView().isFocusableInTouchMode = true + requireView().requestFocus() + requireView().setOnKeyListener { view, keyCode, event -> + if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { + val fromDeviceList = requireArguments().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@setOnKeyListener true + } + } + false + } + } + + private fun refreshUI() { + val device = device ?: return + //Once in-app, there is no point in keep displaying the notification if any + device.hidePairingNotification() + mActivity?.runOnUiThread(object : Runnable { + override fun run() { + if (device.isPairRequestedByPeer) { + requireBinding().pairMessage.setText(R.string.pair_requested) + requireBinding().pairVerification.visibility = View.VISIBLE + requireBinding().pairVerification.text = + SslHelper.getVerificationKey(SslHelper.certificate, device.certificate) + requireBinding().pairingButtons.visibility = View.VISIBLE + requireBinding().pairProgress.visibility = View.GONE + requireBinding().pairButton.visibility = View.GONE + requireBinding().pairRequestButtons.visibility = View.VISIBLE + } else { + val paired = device.isPaired + val reachable = device.isReachable + requireBinding().pairingButtons.visibility = if (paired) View.GONE else View.VISIBLE + if (paired && !reachable) { + requireErrorBinding().errorMessageContainer.visibility = View.VISIBLE + requireErrorBinding().notReachableMessage.visibility = View.VISIBLE + } else { + requireErrorBinding().errorMessageContainer.visibility = View.GONE + requireErrorBinding().notReachableMessage.visibility = View.GONE + } + try { + if (paired && reachable) { + //Plugins button list + val plugins: Collection = device.loadedPlugins.values + pluginListItems.clear() + permissionListItems.clear() + for (p in plugins) { + if (!p.hasMainActivity(context) || p.displayInContextMenu()) continue + pluginListItems.add(PluginItem(p) { p.startMainActivity(mActivity) }) + } + createPermissionsList( + device.pluginsWithoutPermissions, + R.string.plugins_need_permission + ) { plugin: Plugin -> + val dialog = plugin.permissionExplanationDialog + dialog?.show(childFragmentManager, null) + } + createPermissionsList( + device.pluginsWithoutOptionalPermissions, + R.string.plugins_need_optional_permission + ) { plugin: Plugin -> + val dialog: DialogFragment? = plugin.optionalPermissionExplanationDialog + dialog?.show(childFragmentManager, null) + } + + displayBatteryInfoIfPossible() + } + requireDeviceBinding().pluginsList.adapter = ListAdapter(mActivity, pluginListItems) + //don't do unnecessary work when all permissions granted and remove view for landscape orientation + if (permissionListItems.isEmpty()) { + requireDeviceBinding().buttonsList.visibility = View.GONE + } else { + requireDeviceBinding().buttonsList.adapter = ListAdapter(mActivity, permissionListItems) + requireDeviceBinding().buttonsList.visibility = View.VISIBLE + } + mActivity?.invalidateOptionsMenu() + } catch (e: IllegalStateException) { + //Ignore: The activity was closed while we were trying to update it + } catch (e: ConcurrentModificationException) { + Log.e(TAG, "ConcurrentModificationException") + this.run() //Try again + } + } + } + }) + } + + private val pairingCallback: PairingCallback = object : PairingCallback { + override fun incomingRequest() { + refreshUI() + } + + override fun pairingSuccessful() { + refreshUI() + } + + override fun pairingFailed(error: String) { + mActivity?.runOnUiThread { + with(requireBinding()) { + pairMessage.text = error + pairVerification.text = "" + pairVerification.visibility = View.GONE + pairProgress.visibility = View.GONE + pairButton.visibility = View.VISIBLE + pairRequestButtons.visibility = View.GONE + } + refreshUI() + } + } + + override fun unpaired() { + mActivity?.runOnUiThread { + with(requireBinding()) { + pairMessage.setText(R.string.device_not_paired) + pairVerification.visibility = View.GONE + pairProgress.visibility = View.GONE + pairButton.visibility = View.VISIBLE + pairRequestButtons.visibility = View.GONE + } + refreshUI() + } + } + } + + private fun createPermissionsList( + plugins: ConcurrentHashMap, + headerText: Int, + action: FailedPluginListItem.Action + ) { + if (plugins.isEmpty()) return + val device = device ?: return + permissionListItems.add(PluginListHeaderItem(headerText)) + for (plugin in plugins.values) { + if (!device.isPluginEnabled(plugin.pluginKey)) { + continue + } + permissionListItems.add(FailedPluginListItem(plugin, action)) + } + } + + /** + * This method tries to display battery info for the remote device. Includes + * + * * Current charge as a percentage + * * Whether the remote device is low on power + * * Whether the remote device is currently charging + * + * + * + * This will show a simple message on the view instead if we don't have + * accurate info right now. + * + */ + private fun displayBatteryInfoIfPossible() { + val batteryPlugin = device?.loadedPlugins?.get(Plugin.getPluginKey(BatteryPlugin::class.java)) as BatteryPlugin? + + val info = batteryPlugin?.remoteBatteryInfo + if (info != null) { + + @StringRes + val resId: Int = if (info.isCharging) { + R.string.battery_status_charging_format + } else if (BatteryPlugin.isLowBattery(info)) { + R.string.battery_status_low_format + } else { + R.string.battery_status_format + } + + mActivity?.supportActionBar?.subtitle = mActivity?.getString(resId, info.currentCharge) + } else { + mActivity?.supportActionBar?.setSubtitle(R.string.battery_status_unknown) + } + //TODO don't show text if plugin is disabled by user + } + +} diff --git a/src/org/kde/kdeconnect/UserInterface/List/EntryItemWithIcon.java b/src/org/kde/kdeconnect/UserInterface/List/EntryItemWithIcon.java index feb7ebf4..e4420731 100644 --- a/src/org/kde/kdeconnect/UserInterface/List/EntryItemWithIcon.java +++ b/src/org/kde/kdeconnect/UserInterface/List/EntryItemWithIcon.java @@ -12,36 +12,25 @@ import android.view.View; import androidx.annotation.NonNull; -import org.kde.kdeconnect_tp.databinding.ListItemWithIconEntryBinding; +import org.kde.kdeconnect_tp.databinding.ListPluginEntryBinding; public class EntryItemWithIcon implements ListAdapter.Item { protected final String title; - protected final String subtitle; protected final Drawable icon; public EntryItemWithIcon(String title, Drawable icon) { - this(title, null, icon); - } - - protected EntryItemWithIcon(String title, String subtitle, Drawable icon) { this.title = title; - this.subtitle = subtitle; this.icon = icon; } @NonNull @Override public View inflateView(@NonNull LayoutInflater layoutInflater) { - final ListItemWithIconEntryBinding binding = ListItemWithIconEntryBinding.inflate(layoutInflater); + final ListPluginEntryBinding binding = ListPluginEntryBinding.inflate(layoutInflater); binding.listItemEntryTitle.setText(title); binding.listItemEntryIcon.setImageDrawable(icon); - if (subtitle != null) { - binding.listItemEntrySummary.setVisibility(View.VISIBLE); - binding.listItemEntrySummary.setText(subtitle); - } - return binding.getRoot(); } } diff --git a/src/org/kde/kdeconnect/UserInterface/MainActivity.java b/src/org/kde/kdeconnect/UserInterface/MainActivity.java index c65eac3b..2c0ccf06 100644 --- a/src/org/kde/kdeconnect/UserInterface/MainActivity.java +++ b/src/org/kde/kdeconnect/UserInterface/MainActivity.java @@ -350,7 +350,7 @@ public class MainActivity extends AppCompatActivity implements SharedPreferences } else { mNavigationView.setCheckedItem(mCurrentMenuEntry); } - setContentFragment(DeviceFragment.newInstance(deviceId, fromDeviceList)); + setContentFragment(DeviceFragment.Companion.newInstance(deviceId, fromDeviceList)); } else { mCurrentMenuEntry = MENU_ENTRY_ADD_DEVICE; mNavigationView.setCheckedItem(mCurrentMenuEntry);