Split up into modules and add float module
Split the app/ module into three modules terminal/ - Terminal emulator library module. view/ - Terminal view library module (depending on terminal/). app/ - The main Termux app (depending on view/). Also add the float/ - The Termux:Float app (depending on view/).
|
@ -10,6 +10,9 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/float" />
|
||||
<option value="$PROJECT_DIR$/terminal" />
|
||||
<option value="$PROJECT_DIR$/view" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
|
|
|
@ -5,8 +5,9 @@ android {
|
|||
buildToolsVersion "25.0.2"
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.2.0'
|
||||
compile "com.android.support:support-v4:25.2.0"
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile "com.android.support:support-v4:25.3.1"
|
||||
compile project(":view")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
|
@ -15,16 +16,6 @@ android {
|
|||
targetSdkVersion 25
|
||||
versionCode 48
|
||||
versionName "0.48"
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
@ -34,12 +25,6 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package com.termux;
|
||||
|
||||
import android.app.Application;
|
||||
import android.test.ApplicationTestCase;
|
||||
|
||||
/**
|
||||
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
|
||||
*/
|
||||
public class ApplicationTest extends ApplicationTestCase<Application> {
|
||||
public ApplicationTest() {
|
||||
super(Application.class);
|
||||
}
|
||||
}
|
|
@ -256,6 +256,11 @@ public final class TermuxKeyListener implements TerminalKeyListener {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Handle dedicated volume buttons as virtual keys if applicable. */
|
||||
private boolean handleVirtualKeys(int keyCode, KeyEvent event, boolean down) {
|
||||
InputDevice inputDevice = event.getDevice();
|
||||
|
|
|
@ -30,10 +30,6 @@
|
|||
<string name="select_url_copied_to_clipboard">URL copied to clipboard</string>
|
||||
<string name="share_transcript_chooser_title">Send text to:</string>
|
||||
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
|
||||
<string name="kill_process">Kill process (%d)</string>
|
||||
<string name="confirm_kill_process">Really kill this session?</string>
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile project(":view")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.termux.window"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.window"
|
||||
android:sharedUserId="com.termux">
|
||||
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:extractNativeLibs="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme" >
|
||||
<activity
|
||||
android:theme="@android:style/Theme.NoDisplay"
|
||||
android:noHistory="true"
|
||||
android:name=".TermuxFloatActivity"
|
||||
android:label="@string/app_name" >
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:theme="@android:style/Theme.Material"
|
||||
android:name=".TermuxFloatPermissionActivity" />
|
||||
|
||||
<service android:name=".TermuxFloatService" android:exported="false"/>
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -0,0 +1,16 @@
|
|||
package com.termux.window;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
|
||||
/** Simple activity which immediately launches {@link TermuxFloatService} and exits. */
|
||||
public class TermuxFloatActivity extends Activity {
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
startService(new Intent(this, TermuxFloatService.class));
|
||||
finish();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.termux.window;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.View;
|
||||
|
||||
@TargetApi(23)
|
||||
public class TermuxFloatPermissionActivity extends Activity {
|
||||
|
||||
public static int OVERLAY_PERMISSION_REQ_CODE = 1234;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_permission);
|
||||
}
|
||||
|
||||
public void onOkButton(View view) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()));
|
||||
startActivityForResult(intent, OVERLAY_PERMISSION_REQ_CODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == OVERLAY_PERMISSION_REQ_CODE) {
|
||||
startService(new Intent(this, TermuxFloatService.class));
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.termux.window;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.WindowManager;
|
||||
|
||||
public class TermuxFloatPrefs {
|
||||
|
||||
private static final String PREF_X = "window_x";
|
||||
private static final String PREF_Y = "window_y";
|
||||
private static final String PREF_WIDTH = "window_width";
|
||||
private static final String PREF_HEIGHT = "window_height";
|
||||
|
||||
public static void saveWindowSize(Context context, int width, int height) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putInt(PREF_WIDTH, width).putInt(PREF_HEIGHT, height).apply();
|
||||
}
|
||||
|
||||
public static void saveWindowPosition(Context context, int x, int y) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
prefs.edit().putInt(PREF_X, x).putInt(PREF_Y, y).apply();
|
||||
}
|
||||
|
||||
public static void applySavedGeometry(Context context, WindowManager.LayoutParams layout) {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
layout.x = prefs.getInt(PREF_X, 200);
|
||||
layout.y = prefs.getInt(PREF_Y, 200);
|
||||
layout.width = prefs.getInt(PREF_WIDTH, 500);
|
||||
layout.height = prefs.getInt(PREF_HEIGHT, 800);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
package com.termux.window;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.os.IBinder;
|
||||
import android.os.Vibrator;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
public class TermuxFloatService extends Service {
|
||||
|
||||
public static final String ACTION_HIDE = "com.termux.float.hide";
|
||||
public static final String ACTION_SHOW = "com.termux.float.show";
|
||||
|
||||
/**
|
||||
* Note that this is a symlink on the Android M preview.
|
||||
*/
|
||||
@SuppressLint("SdCardPath")
|
||||
public static final String FILES_PATH = "/data/data/com.termux/files";
|
||||
public static final String PREFIX_PATH = FILES_PATH + "/usr";
|
||||
public static final String HOME_PATH = FILES_PATH + "/home";
|
||||
|
||||
/**
|
||||
* The notification id supplied to {@link #startForeground(int, Notification)}.
|
||||
* <p/>
|
||||
* Note that the javadoc for that method says it cannot be zero.
|
||||
*/
|
||||
private static final int NOTIFICATION_ID = 0xdead1337;
|
||||
|
||||
private static final int MIN_FONTSIZE = 16;
|
||||
private static final int DEFAULT_FONTSIZE = 24;
|
||||
private static final String FONTSIZE_KEY = "fontsize";
|
||||
private TermuxFloatView mFloatingWindow;
|
||||
private int mFontSize;
|
||||
private boolean mVisibleWindow = true;
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressLint({"InflateParams"})
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
try {
|
||||
mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(DEFAULT_FONTSIZE)));
|
||||
} catch (NumberFormatException | ClassCastException e) {
|
||||
mFontSize = DEFAULT_FONTSIZE;
|
||||
}
|
||||
|
||||
TermuxFloatView floatingWindow = (TermuxFloatView) ((LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE)).inflate(R.layout.activity_main, null);
|
||||
floatingWindow.initializeFloatingWindow();
|
||||
floatingWindow.mTerminalView.setTextSize(mFontSize);
|
||||
|
||||
TerminalSession session = createTermSession();
|
||||
floatingWindow.mTerminalView.attachSession(session);
|
||||
|
||||
try {
|
||||
floatingWindow.launchFloatingWindow();
|
||||
} catch (Exception e) {
|
||||
// Settings.canDrawOverlays() does not work (always returns false, perhaps due to sharedUserId?).
|
||||
// So instead we catch the exception and prompt here.
|
||||
startActivity(new Intent(this, TermuxFloatPermissionActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
|
||||
mFloatingWindow = floatingWindow;
|
||||
|
||||
Toast toast = Toast.makeText(this, R.string.initial_instruction_toast, Toast.LENGTH_LONG);
|
||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
||||
TextView v = (TextView) toast.getView().findViewById(android.R.id.message);
|
||||
if (v != null) v.setGravity(Gravity.CENTER);
|
||||
toast.show();
|
||||
|
||||
startForeground(NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
final Resources res = getResources();
|
||||
final String contentTitle = res.getString(R.string.notification_title);
|
||||
final String contentText = res.getString(mVisibleWindow ? R.string.notification_message_visible : R.string.notification_message_hidden);
|
||||
|
||||
final String intentAction = mVisibleWindow ? ACTION_HIDE : ACTION_SHOW;
|
||||
Intent actionIntent = new Intent(this, TermuxFloatService.class).setAction(intentAction);
|
||||
|
||||
Notification.Builder builder = new Notification.Builder(this).setContentTitle(contentTitle).setContentText(contentText)
|
||||
.setPriority(Notification.PRIORITY_MIN).setSmallIcon(R.mipmap.ic_service_notification)
|
||||
.setColor(0xFF000000)
|
||||
.setContentIntent(PendingIntent.getService(this, 0, actionIntent, 0))
|
||||
.setOngoing(true).setShowWhen(false);
|
||||
|
||||
//final int messageId = mVisibleWindow ? R.string.toggle_hide : R.string.toggle_show;
|
||||
//builder.addAction(android.R.drawable.ic_menu_preferences, res.getString(messageId), PendingIntent.getService(this, 0, actionIntent, 0));
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("Wakelock")
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_HIDE.equals(action)) {
|
||||
mVisibleWindow = false;
|
||||
mFloatingWindow.setVisibility(View.GONE);
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
||||
} else if (ACTION_SHOW.equals(action)) {
|
||||
mFloatingWindow.setVisibility(View.VISIBLE);
|
||||
mVisibleWindow = true;
|
||||
((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
return Service.START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mFloatingWindow != null) mFloatingWindow.closeFloatingWindow();
|
||||
}
|
||||
|
||||
public void changeFontSize(boolean increase) {
|
||||
mFontSize += (increase ? 1 : -1) * 2;
|
||||
mFontSize = Math.max(MIN_FONTSIZE, mFontSize);
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
|
||||
|
||||
mFloatingWindow.mTerminalView.setTextSize(mFontSize);
|
||||
}
|
||||
|
||||
// XXX: Keep in sync with TermuxService.java.
|
||||
@SuppressLint("SdCardPath")
|
||||
TerminalSession createTermSession() {
|
||||
new File(HOME_PATH).mkdirs();
|
||||
|
||||
String termEnv = "TERM=xterm-256color";
|
||||
String homeEnv = "HOME=" + HOME_PATH;
|
||||
String prefixEnv = "PREFIX=" + PREFIX_PATH;
|
||||
String[] env;
|
||||
String ps1Env = "PS1=$ ";
|
||||
String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
|
||||
String langEnv = "LANG=en_US.UTF-8";
|
||||
String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
|
||||
env = new String[]{termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv};
|
||||
|
||||
String executablePath = null;
|
||||
String[] args;
|
||||
String shellName = null;
|
||||
File shell = new File(HOME_PATH, ".termux/shell");
|
||||
if (shell.exists()) {
|
||||
try {
|
||||
File canonicalFile = shell.getCanonicalFile();
|
||||
if (canonicalFile.isFile() && canonicalFile.canExecute()) {
|
||||
executablePath = canonicalFile.getAbsolutePath();
|
||||
String[] parts = executablePath.split("/");
|
||||
shellName = "-" + parts[parts.length - 1];
|
||||
} else {
|
||||
Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (executablePath == null) {
|
||||
// Try bash, zsh and ash in that order:
|
||||
for (String shellBinary : new String[]{"bash", "zsh", "ash"}) {
|
||||
File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
|
||||
if (shellFile.canExecute()) {
|
||||
executablePath = shellFile.getAbsolutePath();
|
||||
shellName = "-" + shellBinary;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (executablePath == null) {
|
||||
// Fall back to system shell as last resort:
|
||||
executablePath = "/system/bin/sh";
|
||||
shellName = "-sh";
|
||||
}
|
||||
|
||||
args = new String[]{shellName};
|
||||
|
||||
return new TerminalSession(executablePath, HOME_PATH, args, env, new TerminalSession.SessionChangedCallback() {
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession changedSession) {
|
||||
// Ignore for now.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
mFloatingWindow.mTerminalView.onScreenUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(TerminalSession finishedSession) {
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClipboardText(TerminalSession pastingSession, String text) {
|
||||
mFloatingWindow.showToast("Clipboard set:\n\"" + text + "\"", true);
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(text)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession riningSession) {
|
||||
((Vibrator) getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession session) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,277 @@
|
|||
package com.termux.window;
|
||||
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalKeyListener;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.ScaleGestureDetector.OnScaleGestureListener;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Toast;
|
||||
|
||||
public class TermuxFloatView extends LinearLayout {
|
||||
|
||||
public static final float ALPHA_FOCUS = 0.9f;
|
||||
public static final float ALPHA_NOT_FOCUS = 0.7f;
|
||||
public static final float ALPHA_MOVING = 0.5f;
|
||||
|
||||
private int DISPLAY_WIDTH, DISPLAY_HEIGHT;
|
||||
|
||||
final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
|
||||
WindowManager mWindowManager;
|
||||
InputMethodManager imm;
|
||||
|
||||
TerminalView mTerminalView;
|
||||
|
||||
/** The last toast shown, used cancel current toast before showing new in {@link #showToast(String, boolean)}. */
|
||||
Toast mLastToast;
|
||||
|
||||
private boolean withFocus = true;
|
||||
private int initialX;
|
||||
private int initialY;
|
||||
private float initialTouchX;
|
||||
private float initialTouchY;
|
||||
|
||||
boolean isInLongPressState;
|
||||
|
||||
final ScaleGestureDetector mScaleDetector = new ScaleGestureDetector(getContext(), new OnScaleGestureListener() {
|
||||
private static final int MIN_SIZE = 50;
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
int widthChange = (int) (detector.getCurrentSpanX() - detector.getPreviousSpanX());
|
||||
int heightChange = (int) (detector.getCurrentSpanY() - detector.getPreviousSpanY());
|
||||
layoutParams.width += widthChange;
|
||||
layoutParams.height += heightChange;
|
||||
layoutParams.width = Math.max(MIN_SIZE, layoutParams.width);
|
||||
layoutParams.height = Math.max(MIN_SIZE, layoutParams.height);
|
||||
mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams);
|
||||
TermuxFloatPrefs.saveWindowSize(getContext(), layoutParams.width, layoutParams.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
// Do nothing.
|
||||
}
|
||||
});
|
||||
|
||||
public TermuxFloatView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
setAlpha(ALPHA_FOCUS);
|
||||
}
|
||||
|
||||
private static int computeLayoutFlags(boolean withFocus) {
|
||||
if (withFocus) {
|
||||
return 0;
|
||||
} else {
|
||||
return WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
|
||||
}
|
||||
}
|
||||
|
||||
public void initializeFloatingWindow() {
|
||||
mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
|
||||
|
||||
mTerminalView.setOnKeyListener(new TerminalKeyListener() {
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
if (scale < 0.9f || scale > 1.1f) {
|
||||
boolean increase = scale > 1.f;
|
||||
((TermuxFloatService) getContext()).changeFontSize(increase);
|
||||
return 1.0f;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
updateLongPressMode(true);
|
||||
initialX = layoutParams.x;
|
||||
initialY = layoutParams.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
Point displaySize = new Point();
|
||||
getDisplay().getSize(displaySize);
|
||||
DISPLAY_WIDTH = displaySize.x;
|
||||
DISPLAY_HEIGHT = displaySize.y;
|
||||
|
||||
// mTerminalView.checkForFontAndColors();
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
public void launchFloatingWindow() {
|
||||
int widthAndHeight = android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
layoutParams.flags = computeLayoutFlags(true);
|
||||
layoutParams.width = widthAndHeight;
|
||||
layoutParams.height = widthAndHeight;
|
||||
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
|
||||
layoutParams.format = PixelFormat.RGBA_8888;
|
||||
|
||||
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
|
||||
TermuxFloatPrefs.applySavedGeometry(getContext(), layoutParams);
|
||||
|
||||
mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
|
||||
mWindowManager.addView(this, layoutParams);
|
||||
imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
showTouchKeyboard();
|
||||
}
|
||||
|
||||
/** Intercept touch events to obtain and loose focus on touch events. */
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||
if (isInLongPressState) return true;
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
if ((event.getMetaState() & (KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON)) != 0) {
|
||||
updateLongPressMode(true);
|
||||
initialX = layoutParams.x;
|
||||
initialY = layoutParams.y;
|
||||
initialTouchX = event.getRawX();
|
||||
initialTouchY = event.getRawY();
|
||||
return true;
|
||||
}
|
||||
// FIXME: params.x and params.y are outdated when snapping to end of screen, where the movement stops but x
|
||||
// and y are wrong.
|
||||
float touchX = event.getRawX();
|
||||
float touchY = event.getRawY();
|
||||
boolean clickedInside = (touchX >= layoutParams.x) && (touchX <= (layoutParams.x + layoutParams.width)) && (touchY >= layoutParams.y)
|
||||
&& (touchY <= (layoutParams.y + layoutParams.height));
|
||||
if (withFocus != clickedInside) {
|
||||
changeFocus(clickedInside);
|
||||
} else if (clickedInside) {
|
||||
// When clicking inside, show keyboard if the user has hidden it:
|
||||
showTouchKeyboard();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void showTouchKeyboard() {
|
||||
mTerminalView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
imm.showSoftInput(mTerminalView, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateLongPressMode(boolean newValue) {
|
||||
isInLongPressState = newValue;
|
||||
setBackgroundResource(newValue ? R.drawable.floating_window_background_resize : R.drawable.floating_window_background);
|
||||
setAlpha(newValue ? ALPHA_MOVING : ALPHA_FOCUS);
|
||||
if (newValue) {
|
||||
Toast toast = Toast.makeText(getContext(), R.string.after_long_press, Toast.LENGTH_SHORT);
|
||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
||||
toast.show();
|
||||
}
|
||||
}
|
||||
|
||||
/** Motion events should only be dispatched here when {@link #onInterceptTouchEvent(MotionEvent)} returns true. */
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
if (isInLongPressState) {
|
||||
mScaleDetector.onTouchEvent(event);
|
||||
if (mScaleDetector.isInProgress()) return true;
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
layoutParams.x = Math.min(DISPLAY_WIDTH - layoutParams.width, Math.max(0, initialX + (int) (event.getRawX() - initialTouchX)));
|
||||
layoutParams.y = Math.min(DISPLAY_HEIGHT - layoutParams.height, Math.max(0, initialY + (int) (event.getRawY() - initialTouchY)));
|
||||
mWindowManager.updateViewLayout(TermuxFloatView.this, layoutParams);
|
||||
TermuxFloatPrefs.saveWindowPosition(getContext(), layoutParams.x, layoutParams.y);
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
updateLongPressMode(false);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
|
||||
/** Visually indicate focus and show the soft input as needed. */
|
||||
private void changeFocus(boolean newFocus) {
|
||||
withFocus = newFocus;
|
||||
layoutParams.flags = computeLayoutFlags(withFocus);
|
||||
mWindowManager.updateViewLayout(this, layoutParams);
|
||||
setAlpha(newFocus ? ALPHA_FOCUS : ALPHA_NOT_FOCUS);
|
||||
if (newFocus) showTouchKeyboard();
|
||||
}
|
||||
|
||||
/** Show a toast and dismiss the last one if still visible. */
|
||||
void showToast(String text, boolean longDuration) {
|
||||
if (mLastToast != null) mLastToast.cancel();
|
||||
mLastToast = Toast.makeText(getContext(), text, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
|
||||
mLastToast.setGravity(Gravity.TOP, 0, 0);
|
||||
mLastToast.show();
|
||||
}
|
||||
|
||||
public void closeFloatingWindow() {
|
||||
mWindowManager.removeView(this);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Background for the window, used to draw a border -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:color="@android:color/white" />
|
||||
<solid android:color="#FF000000" />
|
||||
</shape>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle" >
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:color="@android:color/holo_red_light" />
|
||||
<solid android:color="#FF000000" />
|
||||
</shape>
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.termux.window.TermuxFloatView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/window_layout"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:background="@drawable/floating_window_background"
|
||||
android:padding="1px" >
|
||||
|
||||
<com.termux.view.TerminalView
|
||||
android:id="@+id/terminal_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:focusable="true"
|
||||
android:focusableInTouchMode="true"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
</com.termux.window.TermuxFloatView>
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:gravity="center"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:layout_gravity="center"
|
||||
android:text="@string/draw_overlay_permission_explanation"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:gravity="center_horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<Button
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/grant_permission"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="onOkButton"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 695 B |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 779 B |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 983 B |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string name="app_name">Termux:Float</string>
|
||||
<string name="initial_instruction_toast">Long press on window to move or resize\n\nDouble tap and drag finger up or down to change font size</string>
|
||||
<string name="after_long_press">Drag to position and pinch to resize</string>
|
||||
|
||||
<string name="draw_overlay_permission_explanation">This app requires permission to draw overlays.</string>
|
||||
<string name="grant_permission">Grant permission</string>
|
||||
|
||||
<string name="notification_title">Termux:Float</string>
|
||||
<string name="notification_message_visible">Touch to hide window.</string>
|
||||
<string name="notification_message_hidden">Touch to show window.</string>
|
||||
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
|
||||
</resources>
|
|
@ -0,0 +1,20 @@
|
|||
<resources>
|
||||
|
||||
<!--
|
||||
Base application theme, dependent on API level. This theme is replaced
|
||||
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
|
||||
-->
|
||||
<style name="AppBaseTheme" parent="android:Theme.Light">
|
||||
<!--
|
||||
Theme customizations available in newer API levels can go in
|
||||
res/values-vXX/styles.xml, while customizations related to
|
||||
backward-compatibility can go here.
|
||||
-->
|
||||
</style>
|
||||
|
||||
<!-- Application theme. -->
|
||||
<style name="AppTheme" parent="AppBaseTheme">
|
||||
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -1 +1 @@
|
|||
include ':app'
|
||||
include ':app', ':terminal', ':view', ':float'
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
|
||||
}
|
||||
}
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
ndkBuild {
|
||||
path "src/main/jni/Android.mk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.terminal">
|
||||
</manifest>
|
|
@ -0,0 +1,36 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-annotations:25.3.1'
|
||||
compile project(":terminal")
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 25
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||
testCompile 'junit:junit:4.12'
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Users/fornwall/lib/android-sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.view">
|
||||
</manifest>
|
|
@ -14,10 +14,14 @@ import com.termux.terminal.TerminalSession;
|
|||
*/
|
||||
public interface TerminalKeyListener {
|
||||
|
||||
/** Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}. */
|
||||
/**
|
||||
* Callback function on scale events according to {@link ScaleGestureDetector#getScaleFactor()}.
|
||||
*/
|
||||
float onScale(float scale);
|
||||
|
||||
/** On a single tap on the terminal if terminal mouse reporting not enabled. */
|
||||
/**
|
||||
* On a single tap on the terminal if terminal mouse reporting not enabled.
|
||||
*/
|
||||
void onSingleTapUp(MotionEvent e);
|
||||
|
||||
boolean shouldBackButtonBeMappedToEscape();
|
||||
|
@ -34,4 +38,6 @@ public interface TerminalKeyListener {
|
|||
|
||||
boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session);
|
||||
|
||||
boolean onLongPress(MotionEvent event);
|
||||
|
||||
}
|
|
@ -29,7 +29,6 @@ import android.view.inputmethod.EditorInfo;
|
|||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.Scroller;
|
||||
|
||||
import com.termux.R;
|
||||
import com.termux.terminal.EmulatorDebug;
|
||||
import com.termux.terminal.KeyHandler;
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
|
@ -189,6 +188,7 @@ public final class TerminalView extends View {
|
|||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
if (mOnKeyListener.onLongPress(e)) return;
|
||||
if (!mGestureRecognizer.isInProgress() && !mIsSelectingText) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||
toggleSelectingText(e);
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,5 @@
|
|||
<resources>
|
||||
<string name="paste_text">Paste</string>
|
||||
<string name="copy_text">Copy</string>
|
||||
<string name="text_selection_more">More…</string>
|
||||
</resources>
|