mirror of
https://github.com/KDE/kdeconnect-android
synced 2025-08-22 01:51:47 +00:00
Accept data via Intent and pass them on to the host as keystrokes
Adds new functionality to the MousepadPlugin to accept data via Intent and pass them on to the host as keystrokes via the existing MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST PackageType eg. to easily send OTP codes from the phone to the Desktop
This commit is contained in:
parent
f41360a7fc
commit
801b6451ed
@ -255,6 +255,20 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.kde.kdeconnect.Plugins.MousePadPlugin.SendKeystrokesToHostActivity"
|
||||
android:label="@string/pref_plugin_mousepad_send_keystrokes"
|
||||
android:parentActivityName="org.kde.kdeconnect.UserInterface.MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="org.kde.kdeconnect.UserInterface.MainActivity" />
|
||||
<!-- Accept data with "text/x-keystrokes" to send the text to the connected host and emulate keystrokes -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<data android:mimeType="text/x-keystrokes"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="org.kde.kdeconnect.Plugins.PresenterPlugin.PresenterActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
|
49
res/layout/activity_sendkeystrokes.xml
Normal file
49
res/layout/activity_sendkeystrokes.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout 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:id="@+id/sendkeystrokes_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:keepScreenOn="true"
|
||||
android:orientation="vertical"
|
||||
android:padding="4dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="5dp"
|
||||
app:errorEnabled="true">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/textToSend"
|
||||
style="@style/Widget.MaterialComponents.TextInputEditText.FilledBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:cursorVisible="true"
|
||||
android:hint="@string/sendkeystrokes_textbox_hint"
|
||||
android:inputType="text"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:scrollHorizontally="true"
|
||||
android:text="" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
|
||||
<ListView
|
||||
android:id="@+id/devices_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:addStatesFromChildren="true"
|
||||
android:divider="@null"
|
||||
android:dividerHeight="0dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dip"
|
||||
tools:context=".MainActivity" />
|
||||
|
||||
|
||||
</LinearLayout>
|
@ -110,6 +110,26 @@
|
||||
<item>strong</item>
|
||||
<item>stronger</item>
|
||||
</string-array>
|
||||
|
||||
<string name="sendkeystrokes_send_to">Send keystrokes to</string>
|
||||
<string name="sendkeystrokes_textbox_hint">Send keystrokes to host</string>
|
||||
<string name="sendkeystrokes_disabled_toast">Sending keystrokes is disabled - enable it in the settings</string>
|
||||
<string name="sendkeystrokes_wrong_data"><![CDATA[Invalid mime type - needs to be \'text/x-keystrokes\']]></string>
|
||||
<string name="sendkeystrokes_sent_text">Sent %1s to device %2s</string>
|
||||
|
||||
<string name="sendkeystrokes_pref_category_summary">This module allows other apps to share text segments as keystrokes which will get send to the connected host</string>
|
||||
<string name="sendkeystrokes_pref_category_title">Send Keystrokes</string>
|
||||
<string name="sendkeystrokes_pref_enabled">Enable Keystrokes sending</string>
|
||||
<string name="sendkeystrokes_pref_enabled_summary"><![CDATA[Listen for data with mime type \'text/x-keystrokes\']]></string>
|
||||
<string name="sendkeystrokes_safe_text_enabled">Send safe text immediately</string>
|
||||
<string name="sendkeystrokes_safe_text_enabled_summary">Send short only-numeric strings without confirmation</string>
|
||||
|
||||
|
||||
<string name="pref_plugin_mousepad_send_keystrokes">Send as keystrokes</string>
|
||||
<string name="sendkeystrokes_pref_category" translatable="false">category_send_keystrokes</string>
|
||||
<string name="pref_sendkeystrokes_enabled" translatable="false">pref_sendkeystrokes_enabled</string>
|
||||
<string name="pref_send_safe_text_immediately" translatable="false">pref_send_safe_text_immediately</string>
|
||||
|
||||
<string name="category_connected_devices">Connected devices</string>
|
||||
<string name="category_not_paired_devices">Available devices</string>
|
||||
<string name="category_remembered_devices">Remembered devices</string>
|
||||
|
@ -46,4 +46,28 @@
|
||||
android:defaultValue="false"
|
||||
android:key="@string/mousepad_scroll_direction"
|
||||
android:title="@string/mousepad_scroll_direction_title" />
|
||||
|
||||
|
||||
<org.kde.kdeconnect.Helpers.LongSummaryPreferenceCategory
|
||||
android:key="@string/sendkeystrokes_pref_category"
|
||||
android:summary="@string/sendkeystrokes_pref_category_summary"
|
||||
android:title="@string/sendkeystrokes_pref_category_title">
|
||||
|
||||
<CheckBoxPreference
|
||||
android:id="@+id/pref_keystrokes_enable"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_sendkeystrokes_enabled"
|
||||
android:title="@string/sendkeystrokes_pref_enabled"
|
||||
android:summary="@string/sendkeystrokes_pref_enabled_summary"
|
||||
/>
|
||||
|
||||
<CheckBoxPreference
|
||||
android:id="@+id/pref_send_safe_text_immediately"
|
||||
android:defaultValue="true"
|
||||
android:key="@string/pref_send_safe_text_immediately"
|
||||
android:title="@string/sendkeystrokes_safe_text_enabled"
|
||||
android:summary="@string/sendkeystrokes_safe_text_enabled_summary"
|
||||
/>
|
||||
|
||||
</org.kde.kdeconnect.Helpers.LongSummaryPreferenceCategory>
|
||||
</PreferenceScreen>
|
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Helpers;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
// the default Preference Category only shows a one-line summary
|
||||
public class LongSummaryPreferenceCategory extends PreferenceCategory {
|
||||
public LongSummaryPreferenceCategory(Context ctx, AttributeSet attrs, int defStyle) {
|
||||
super(ctx, attrs, defStyle);
|
||||
}
|
||||
|
||||
public LongSummaryPreferenceCategory(Context ctx, AttributeSet attrs) {
|
||||
super(ctx, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
TextView summary = (TextView) holder.findViewById(android.R.id.summary);
|
||||
summary.setMaxLines(3);
|
||||
summary.setSingleLine(false);
|
||||
}
|
||||
|
||||
}
|
42
src/org/kde/kdeconnect/Helpers/SafeTextChecker.java
Normal file
42
src/org/kde/kdeconnect/Helpers/SafeTextChecker.java
Normal file
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Helpers;
|
||||
|
||||
public class SafeTextChecker {
|
||||
|
||||
private final String safeChars;
|
||||
private final Integer maxLength;
|
||||
|
||||
public SafeTextChecker(String safeChars, Integer maxLength) {
|
||||
this.safeChars = safeChars;
|
||||
this.maxLength = maxLength;
|
||||
}
|
||||
|
||||
|
||||
// is used by the SendKeystrokes functionality to evaluate if a to-be-send text is safe for
|
||||
// sending without user confirmation
|
||||
// only allow sending text that can not harm any connected desktop (like "format c:\n" / "rm -rf\n",...)
|
||||
public boolean isSafe(String content) {
|
||||
if (content == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (content.length() > maxLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < content.length(); i++) {
|
||||
String charAtPos = content.substring(i, i + 1);
|
||||
if (!safeChars.contains(charAtPos)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// we are happy with the string
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,179 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Plugins.MousePadPlugin;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import org.kde.kdeconnect.BackgroundService;
|
||||
import org.kde.kdeconnect.Device;
|
||||
import org.kde.kdeconnect.Helpers.SafeTextChecker;
|
||||
import org.kde.kdeconnect.NetworkPacket;
|
||||
import org.kde.kdeconnect.UserInterface.List.EntryItem;
|
||||
import org.kde.kdeconnect.UserInterface.List.ListAdapter;
|
||||
import org.kde.kdeconnect.UserInterface.List.SectionItem;
|
||||
import org.kde.kdeconnect.UserInterface.ThemeUtil;
|
||||
import org.kde.kdeconnect_tp.R;
|
||||
import org.kde.kdeconnect_tp.databinding.ActivitySendkeystrokesBinding;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SendKeystrokesToHostActivity extends AppCompatActivity {
|
||||
|
||||
// text with these length and content can be send without user confirmation.
|
||||
// more or less chosen arbitrarily, so that we allow short PINS and TANS without interruption (if only one device is connected)
|
||||
// but also be on the safe side, so that apps cant send any harmful content
|
||||
public static final int MAX_SAFE_LENGTH = 8;
|
||||
public static final String SAFE_CHARS = "1234567890";
|
||||
|
||||
|
||||
private ActivitySendkeystrokesBinding binding;
|
||||
private boolean contentIsOkay;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
ThemeUtil.setUserPreferredTheme(this);
|
||||
|
||||
binding = ActivitySendkeystrokesBinding.inflate(getLayoutInflater());
|
||||
setContentView(binding.getRoot());
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1) // needed for this.getReferrer()
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
if (!prefs.getBoolean(getString(R.string.pref_sendkeystrokes_enabled), true)) {
|
||||
Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_disabled_toast, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
} else {
|
||||
final Intent intent = getIntent();
|
||||
String type = intent.getType();
|
||||
|
||||
if ("text/x-keystrokes".equals(type)) {
|
||||
String toSend = intent.getStringExtra(Intent.EXTRA_TEXT);
|
||||
binding.textToSend.setText(toSend);
|
||||
|
||||
// if the preference send_safe_text_immediately is true, we will check if exactly one
|
||||
// device is connected and send the text to it without user confirmation, to make sending of
|
||||
// short and safe text like PINs/TANs very fluent
|
||||
//
|
||||
// (contentIsOkay gets used in updateComputerList again)
|
||||
if (prefs.getBoolean(getString(R.string.pref_send_safe_text_immediately), true)) {
|
||||
SafeTextChecker safeTextChecker = new SafeTextChecker(SAFE_CHARS, MAX_SAFE_LENGTH);
|
||||
contentIsOkay = safeTextChecker.isSafe(toSend);
|
||||
} else {
|
||||
contentIsOkay = false;
|
||||
}
|
||||
|
||||
// If we trust the sending app, check if there is only one device paired / reachable...
|
||||
if (contentIsOkay) {
|
||||
List<Device> reachableDevices = BackgroundService.getInstance().getDevices().values().stream()
|
||||
.filter(Device::isReachable)
|
||||
.limit(2) // we only need the first two; if its more than one, we need to show the user the device-selection
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// if its exactly one just send the text to it
|
||||
if (reachableDevices.size() == 1) {
|
||||
// send the text and close this activity
|
||||
sendKeys(reachableDevices.get(0));
|
||||
this.finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// subscribe to new connected devices
|
||||
BackgroundService.RunCommand(this, service -> {
|
||||
service.onNetworkChange();
|
||||
service.addDeviceListChangedCallback("SendKeystrokesToHostActivity", this::updateComputerList);
|
||||
});
|
||||
|
||||
// list all currently connected devices
|
||||
updateComputerList();
|
||||
|
||||
} else {
|
||||
Toast.makeText(getApplicationContext(), R.string.sendkeystrokes_wrong_data, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
BackgroundService.RunCommand(this, service -> service.removeDeviceListChangedCallback("SendKeystrokesToHostActivity"));
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
private void sendKeys(Device deviceId) {
|
||||
String toSend;
|
||||
if (binding.textToSend.getText() != null && (toSend = binding.textToSend.getText().toString().trim()).length() > 0) {
|
||||
final NetworkPacket np = new NetworkPacket(MousePadPlugin.PACKET_TYPE_MOUSEPAD_REQUEST);
|
||||
np.set("key", toSend);
|
||||
BackgroundService.RunWithPlugin(this, deviceId.getDeviceId(), MousePadPlugin.class, plugin -> plugin.sendKeyboardPacket(np));
|
||||
Toast.makeText(
|
||||
getApplicationContext(),
|
||||
getString(R.string.sendkeystrokes_sent_text, toSend, deviceId.getName()),
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void updateComputerList() {
|
||||
BackgroundService.RunCommand(this, service -> {
|
||||
|
||||
Collection<Device> devices = service.getDevices().values();
|
||||
final ArrayList<Device> devicesList = new ArrayList<>();
|
||||
final ArrayList<ListAdapter.Item> items = new ArrayList<>();
|
||||
|
||||
SectionItem section = new SectionItem(getString(R.string.sendkeystrokes_send_to));
|
||||
items.add(section);
|
||||
|
||||
for (Device d : devices) {
|
||||
if (d.isReachable() && d.isPaired()) {
|
||||
devicesList.add(d);
|
||||
items.add(new EntryItem(d.getName()));
|
||||
section.isEmpty = false;
|
||||
}
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
binding.devicesList.setAdapter(new ListAdapter(SendKeystrokesToHostActivity.this, items));
|
||||
binding.devicesList.setOnItemClickListener((adapterView, view, i, l) -> {
|
||||
Device device = devicesList.get(i - 1); // NOTE: -1 because of the title!
|
||||
sendKeys(device);
|
||||
this.finish(); // close the activity
|
||||
});
|
||||
});
|
||||
|
||||
// only one device is connected and we trust the text to send -> send it and close the activity.
|
||||
// Usually we already check it in `onStart` - but if the BackgroundService was not started/connected to the host
|
||||
// it will not have the deviceList in memory. Use this callback as second chance (but it will flicker a bit, because the activity might
|
||||
// already been visible and get closed again quickly)
|
||||
if (devicesList.size() == 1 && contentIsOkay) {
|
||||
Device device = devicesList.get(0);
|
||||
sendKeys(device);
|
||||
this.finish(); // close the activity
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
36
tests/org/kde/kdeconnect/Helpers/SafeTextCheckerTest.java
Normal file
36
tests/org/kde/kdeconnect/Helpers/SafeTextCheckerTest.java
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2021 Daniel Weigl <DanielWeigl@gmx.at>
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
|
||||
*/
|
||||
|
||||
package org.kde.kdeconnect.Helpers;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
public class SafeTextCheckerTest extends TestCase {
|
||||
|
||||
public void testSafeTextChecker() {
|
||||
SafeTextChecker safeTextChecker = new SafeTextChecker("1234567890", 8);
|
||||
assertIsOkay("123456", safeTextChecker);
|
||||
assertIsOkay("123", safeTextChecker);
|
||||
assertIsOkay("12345678", safeTextChecker);
|
||||
assertIsOkay("", safeTextChecker);
|
||||
|
||||
assertIsNotOkay(null, safeTextChecker);
|
||||
assertIsNotOkay("123456789", safeTextChecker);
|
||||
assertIsNotOkay("123o", safeTextChecker);
|
||||
assertIsNotOkay("O123", safeTextChecker); // its a O not a 0
|
||||
assertIsNotOkay("o", safeTextChecker);
|
||||
assertIsNotOkay(" ", safeTextChecker);
|
||||
assertIsNotOkay("12345678 ", safeTextChecker);
|
||||
}
|
||||
|
||||
private void assertIsOkay(String text, SafeTextChecker stc) {
|
||||
assertTrue(text + " should be okay", stc.isSafe(text));
|
||||
}
|
||||
|
||||
private void assertIsNotOkay(String text, SafeTextChecker stc) {
|
||||
assertFalse(text + " should not be okay", stc.isSafe(text));
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user