2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-08-22 09:58:08 +00:00

Show enabled plugins as cards

This commit is contained in:
Dmitry Yudin 2023-03-29 18:22:27 +00:00 committed by Albert Vaca Cintora
parent aef5af30ed
commit 0ed11d2036
11 changed files with 498 additions and 506 deletions

View File

@ -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">
<!-- Layout shown when device is reachable but not yet paired -->
@ -18,51 +18,27 @@
layout="@layout/view_pair_error"
tools:visibility="gone"/>
<!-- Layout shown when device is paired and reachable -->
<!-- Layouts shown when device is paired and reachable -->
<GridView
android:id="@+id/plugins_list"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="@integer/plugins_list_weight"
android:numColumns="@integer/plugins_columns"
android:horizontalSpacing="8dp"
android:verticalSpacing="8dp"
android:layout_margin="@dimen/activity_vertical_margin"
tools:listitem="@layout/list_plugin_entry"
tools:layout_height="300dp" />
<ListView
android:id="@+id/buttons_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
android:layout_height="wrap_content"
android:layout_weight="@integer/buttons_list_weight"
android:divider="@null"
android:dividerHeight="0dp"
tools:context=".DeviceActivity"
tools:listitem="@layout/list_item_with_icon_entry"
android:layout_weight=".8"
android:divider="@null"
android:dividerHeight="0dp" />
<!-- Extra information about the current device -->
<RelativeLayout
android:id="@+id/view_status_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
android:padding="8dp"
android:visibility="gone"
tools:visibility="visible"
tools:ignore="UnusedAttribute">
<TextView
android:id="@+id/view_status_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:padding="4dp"
android:text="@string/view_status_title"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
/>
<CheckedTextView
android:id="@+id/view_battery_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/view_status_title"
android:checkMark="@null"
android:clickable="false"
android:padding="4dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:text="@string/battery_status_unknown"
tools:text="100%"
/>
</RelativeLayout>
</LinearLayout>
tools:layout_height="300dp" />
</LinearLayout>

View File

@ -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">
<ListView
android:id="@+id/devices_list"
@ -11,8 +12,9 @@
android:layout_height="match_parent"
android:addStatesFromChildren="true"
android:divider="@null"
android:dividerHeight="0dp"
android:dividerHeight="12dp"
android:orientation="vertical"
tools:listitem="@layout/list_plugin_entry"
tools:context=".MainActivity" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.Material3.CardView.Filled"
app:contentPadding="12dp"
tools:layout_width="240dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="center_vertical"
android:layout_marginTop="8dp"
android:minHeight="?android:attr/listPreferredItemHeight"
android:orientation="vertical">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/list_item_entry_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:contentDescription="@string/device_icon_description"
android:src="@drawable/ic_device_laptop_32dp"
android:layout_marginBottom="12dp"
app:tint="?attr/colorOnSurface" />
<TextView
android:id="@+id/list_item_entry_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:ellipsize="end"
android:lines="2"
android:text=""
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/colorOnSurface"
tools:maxLength="30"
tools:text="@tools:sample/lorem/random" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="activity_device_orientation">@integer/orientation_horizontal</integer>
<integer name="plugins_list_weight">4</integer>
<integer name="buttons_list_weight">6</integer>
</resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<integer name="plugins_columns">3</integer>
</resources>

14
res/values/consts.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- https://developer.android.com/reference/android/R.attr.html#orientation -->
<integer name="orientation_horizontal">0</integer>
<integer name="orientation_vertical">1</integer>
<!--used in activity_device-->
<integer name="activity_device_orientation">@integer/orientation_vertical</integer>
<integer name="plugins_list_weight">@null</integer>
<integer name="buttons_list_weight">@null</integer>
<integer name="plugins_columns">2</integer>
</resources>

View File

@ -315,6 +315,7 @@
<string name="plugins_need_permission">Some Plugins need permissions to work (tap for more info):</string>
<string name="permission_explanation">This plugin needs permissions to work</string>
<string name="all_permissions_granted">All permissions granted 🎉</string>
<string name="optional_permission_explanation">You need to grant extra permissions to enable all functions</string>
<string name="plugins_need_optional_permission">Some plugins have features disabled because of lack of permission (tap for more info):</string>
<string name="share_optional_permission_explanation">To receive files you need to allow storage access</string>

View File

@ -1,446 +0,0 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* 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<ListAdapter.Item> 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<Plugin> 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<Plugin> 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<String, Plugin> 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
* <ul>
* <li>Current charge as a percentage</li>
* <li>Whether the remote device is low on power</li>
* <li>Whether the remote device is currently charging</li>
* </ul>
* <p>
* This will show a simple message on the view instead if we don't have
* accurate info right now.
* </p>
*/
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);
}
}
}

View File

@ -0,0 +1,398 @@
/*
* SPDX-FileCopyrightText: 2014 Albert Vaca Cintora <albertvaka@gmail.com>
*
* 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<ListAdapter.Item> = ArrayList()
private val permissionListItems: ArrayList<ListAdapter.Item> = 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<Plugin> = 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<Plugin> = 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<String, Plugin>,
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
}
}

View File

@ -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();
}
}

View File

@ -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);