2
0
mirror of https://github.com/KDE/kdeconnect-android synced 2025-09-08 01:55:08 +00:00
Files
kdeconnect-android/src/org/kde/kdeconnect/UserInterface/DeviceFragment.java
Bacteria Dev 118d8784fa Hide 'Send Clipboard' from DeviceFragment when READ_LOGS permission is granted
On Android 10 and above, Clipboard needs to be manually sent. Recently the app was shipped with the patches so that users can use adb to grant the required permissions for automatic clipboard propagation.

In case the permissions were granted for automatic clipboard propagation, 'Send Clipboard' option was hidden from persistent notification but was not removed from the DeviceFragment. This MR addresses this.
2023-03-04 12:34:59 +00:00

447 lines
17 KiB
Java

/*
* 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);
}
}
}