File indexing completed on 2024-12-22 04:41:39

0001 /*
0002  * SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
0003  *
0004  * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 package org.kde.kdeconnect.Plugins.MousePadPlugin;
0008 
0009 import android.content.Intent;
0010 import android.content.SharedPreferences;
0011 import android.os.Build;
0012 import android.os.Bundle;
0013 import android.preference.PreferenceManager;
0014 import android.widget.Toast;
0015 
0016 import androidx.annotation.RequiresApi;
0017 import androidx.appcompat.app.AppCompatActivity;
0018 
0019 import org.kde.kdeconnect.BackgroundService;
0020 import org.kde.kdeconnect.Device;
0021 import org.kde.kdeconnect.Helpers.SafeTextChecker;
0022 import org.kde.kdeconnect.KdeConnect;
0023 import org.kde.kdeconnect.NetworkPacket;
0024 import org.kde.kdeconnect.UserInterface.List.EntryItemWithIcon;
0025 import org.kde.kdeconnect.UserInterface.List.ListAdapter;
0026 import org.kde.kdeconnect.UserInterface.List.SectionItem;
0027 import org.kde.kdeconnect_tp.R;
0028 import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding;
0029 
0030 import java.util.ArrayList;
0031 import java.util.Collection;
0032 import java.util.List;
0033 import java.util.Objects;
0034 import java.util.stream.Collectors;
0035 
0036 public class SendKeystrokesToHostActivity extends AppCompatActivity {
0037 
0038     // text with these length and content can be send without user confirmation.
0039     // more or less chosen arbitrarily, so that we allow short PINS and TANS without interruption (if only one device is connected)
0040     // but also be on the safe side, so that apps cant send any harmful content
0041     public static final int MAX_SAFE_LENGTH = 8;
0042     public static final String SAFE_CHARS = "1234567890";
0043 
0044 
0045     private ActivitySendkeystrokesBinding binding;
0046     private boolean contentIsOkay;
0047 
0048     @Override
0049     protected void onCreate(Bundle savedInstanceState) {
0050         super.onCreate(savedInstanceState);
0051 
0052         binding = ActivitySendkeystrokesBinding.inflate(getLayoutInflater());
0053         setContentView(binding.getRoot());
0054 
0055         setSupportActionBar(binding.toolbarLayout.toolbar);
0056         Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
0057         getSupportActionBar().setDisplayShowHomeEnabled(true);
0058     }
0059 
0060 
0061     @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) // needed for this.getReferrer()
0062     @Override
0063     protected void onStart() {
0064         super.onStart();
0065 
0066         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
0067         if (!prefs.getBoolean(getString(R.string.pref_sendkeystrokes_enabled), true)) {
0068             Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_disabled_toast, Toast.LENGTH_LONG).show();
0069             finish();
0070         } else {
0071             final Intent intent = getIntent();
0072             String type = intent.getType();
0073 
0074             if ("text/x-keystrokes".equals(type)) {
0075                 String toSend = intent.getStringExtra(Intent.EXTRA_TEXT);
0076                 binding.textToSend.setText(toSend);
0077 
0078                 // if the preference send_safe_text_immediately is true, we will check if exactly one
0079                 // device is connected and send the text to it without user confirmation, to make sending of
0080                 // short and safe text like PINs/TANs very fluent
0081                 //
0082                 // (contentIsOkay gets used in updateDeviceList again)
0083                 if (prefs.getBoolean(getString(R.string.pref_send_safe_text_immediately), true)) {
0084                     SafeTextChecker safeTextChecker = new SafeTextChecker(SAFE_CHARS, MAX_SAFE_LENGTH);
0085                     contentIsOkay = safeTextChecker.isSafe(toSend);
0086                 } else {
0087                     contentIsOkay = false;
0088                 }
0089 
0090                 // If we trust the sending app, check if there is only one device paired / reachable...
0091                 if (contentIsOkay) {
0092                     List<Device> reachableDevices = KdeConnect.getInstance().getDevices().values().stream()
0093                             .filter(Device::isReachable)
0094                             .limit(2)  // we only need the first two; if its more than one, we need to show the user the device-selection
0095                             .collect(Collectors.toList());
0096 
0097                     // if its exactly one just send the text to it
0098                     if (reachableDevices.size() == 1) {
0099                         // send the text and close this activity
0100                         sendKeys(reachableDevices.get(0));
0101                         this.finish();
0102                         return;
0103                     }
0104                 }
0105 
0106                 KdeConnect.getInstance().addDeviceListChangedCallback("SendKeystrokesToHostActivity", () -> runOnUiThread(this::updateDeviceList));
0107                 BackgroundService.ForceRefreshConnections(this); // force a network re-discover
0108                 updateDeviceList();
0109             } else {
0110                 Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_wrong_data, Toast.LENGTH_LONG).show();
0111                 finish();
0112             }
0113         }
0114     }
0115 
0116     @Override
0117     protected void onStop() {
0118         KdeConnect.getInstance().removeDeviceListChangedCallback("SendKeystrokesToHostActivity");
0119         super.onStop();
0120     }
0121 
0122     private void sendKeys(Device deviceId) {
0123         String toSend;
0124         if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) {
0125             final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
0126             np.set("key", toSend);
0127             MousePadPlugin plugin = KdeConnect.getInstance().getDevicePlugin(deviceId.getDeviceId(), MousePadPlugin.class);
0128             if (plugin == null) {
0129                 finish();
0130                 return;
0131             }
0132             plugin.sendKeyboardPacket(np);
0133             Toast.makeText(
0134                     getApplicationContext(),
0135                     getString(R.string.sendkeystrokes_sent_text, toSend, deviceId.getName()),
0136                     Toast.LENGTH_SHORT
0137             ).show();
0138 
0139         }
0140     }
0141 
0142 
0143     private void updateDeviceList() {
0144         Collection<Device> devices = KdeConnect.getInstance().getDevices().values();
0145         final ArrayList<Device> devicesList = new ArrayList<>();
0146         final ArrayList<ListAdapter.Item> items = new ArrayList<>();
0147 
0148         SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to));
0149         items.add(section);
0150 
0151         for (Device d : devices) {
0152             if (d.isReachable() && d.isPaired()) {
0153                 devicesList.add(d);
0154                 items.add(new EntryItemWithIcon(d.getName(), d.getIcon()));
0155                 section.isEmpty = false;
0156             }
0157         }
0158 
0159         binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
0160         binding.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
0161             Device device = devicesList.get(i - 1); // NOTE: -1 because of the title!
0162             sendKeys(device);
0163             this.finish(); // close the activity
0164         });
0165 
0166         // only one device is connected and we trust the text to send -> send it and close the activity.
0167         // Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host
0168         // it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might
0169         // already been visible and get closed again quickly)
0170         if (devicesList.size() == 1 && contentIsOkay) {
0171             Device device = devicesList.get(0);
0172             sendKeys(device);
0173             this.finish(); // close the activity
0174         }
0175     }
0176 }
0177