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:
parent
aef5af30ed
commit
0ed11d2036
@ -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>
|
@ -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>
|
43
res/layout/list_plugin_entry.xml
Normal file
43
res/layout/list_plugin_entry.xml
Normal 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>
|
9
res/values-land/consts.xml
Normal file
9
res/values-land/consts.xml
Normal 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>
|
6
res/values-w820dp/consts.xml
Normal file
6
res/values-w820dp/consts.xml
Normal 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
14
res/values/consts.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
398
src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt
Normal file
398
src/org/kde/kdeconnect/UserInterface/DeviceFragment.kt
Normal 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
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user