Create termux-shared library package for all termux constants and shared utils

The termux plugins should use this library instead of hardcoding "com.termux" values in their source code.

The library can be included as a dependency by plugins and third party apps by including the following line in the build.gradle where x.xxx is the version number, once its published.

`implementation 'com.termux:termux-shared:x.xxx'`

The `TermuxConstants` class has been updated to `v0.17.0`, `TermuxPreferenceConstants` to `v0.9.0` and `TermuxPropertyConstants` to `v0.6.0`. Check their Changelog sections for info on changes.

Some typos and redundant code has also been fixed.
This commit is contained in:
agnostic-apollo
2021-04-07 11:31:30 +05:00
parent c9a476caf7
commit 682ce08314
73 changed files with 746 additions and 520 deletions

1
termux-shared/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,67 @@
apply plugin: 'com.android.library'
apply plugin: 'maven-publish'
android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
dependencies {
implementation "androidx.annotation:annotation:1.2.0"
implementation "com.google.guava:guava:24.1-jre"
implementation "io.noties.markwon:core:$markwonVersion"
implementation "io.noties.markwon:ext-strikethrough:$markwonVersion"
implementation "io.noties.markwon:linkify:$markwonVersion"
implementation "io.noties.markwon:recycler:$markwonVersion"
// Do not increment version higher than 2.5 or there
// will be runtime exceptions on android < 8
// due to missing classes like java.nio.file.Path.
implementation "commons-io:commons-io:2.5"
}
defaultConfig {
minSdkVersion project.properties.minSdkVersion.toInteger()
targetSdkVersion project.properties.targetSdkVersion.toInteger()
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
testImplementation "junit:junit:4.13.2"
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}
publishing {
publications {
bar(MavenPublication) {
groupId 'com.termux'
artifactId 'termux-shared'
version '0.108'
artifact("$buildDir/outputs/aar/termux-shared-release.aar")
}
}
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/termux/termux-app")
credentials {
username = System.getenv("GH_USERNAME")
password = System.getenv("GH_TOKEN")
}
}
}
}

10
termux-shared/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,10 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
-dontobfuscate
#-renamesourcefileattribute SourceFile
#-keepattributes SourceFile,LineNumberTable

View File

@@ -0,0 +1,26 @@
package com.termux.shared;
import android.content.Context;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.termux.shared.test", appContext.getPackageName());
}
}

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.termux.shared">
</manifest>

View File

@@ -0,0 +1,74 @@
package com.termux.shared.crash;
import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.termux.TermuxUtils;
import java.nio.charset.Charset;
/**
* Catches uncaught exceptions and logs them.
*/
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private final Context context;
private final Thread.UncaughtExceptionHandler defaultUEH;
private static final String LOG_TAG = "CrashUtils";
private CrashHandler(final Context context) {
this.context = context;
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
}
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
logCrash(context,thread, throwable);
defaultUEH.uncaughtException(thread, throwable);
}
/**
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
*/
public static void setCrashHandler(final Context context) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context));
}
}
/**
* Log a crash in the crash log file at
* {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
*
* @param context The {@link Context} for operations.
* @param thread The {@link Thread} in which the crash happened.
* @param throwable The {@link Throwable} thrown for the crash.
*/
public static void logCrash(final Context context, final Thread thread, final Throwable throwable) {
StringBuilder reportString = new StringBuilder();
reportString.append("## Crash Details\n");
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Thread", thread.toString(), "-"));
reportString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Crash Timestamp", TermuxUtils.getCurrentTimeStamp(), "-"));
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTraceStringArray(throwable)));
reportString.append("\n\n").append(TermuxUtils.getAppInfoMarkdownString(context, true));
reportString.append("\n\n").append(TermuxUtils.getDeviceInfoMarkdownString(context));
// Log report string to logcat
Logger.logError(reportString.toString());
// Write report string to crash log file
String errmsg = FileUtils.writeStringToFile(context, "crash log", TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH, Charset.defaultCharset(), reportString.toString(), false);
if (errmsg != null) {
Logger.logError(LOG_TAG, errmsg);
}
}
}

View File

@@ -0,0 +1,217 @@
package com.termux.shared.data;
import android.os.Bundle;
import java.util.LinkedHashSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DataUtils {
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
public static String getTruncatedCommandOutput(String text, int maxLength, boolean fromEnd, boolean onNewline, boolean addPrefix) {
if (text == null) return null;
String prefix = "(truncated) ";
if (addPrefix)
maxLength = maxLength - prefix.length();
if (maxLength < 0 || text.length() < maxLength) return text;
if (fromEnd) {
text = text.substring(0, Math.min(text.length(), maxLength));
} else {
int cutOffIndex = text.length() - maxLength;
if (onNewline) {
int nextNewlineIndex = text.indexOf('\n', cutOffIndex);
if (nextNewlineIndex != -1 && nextNewlineIndex != text.length() - 1) {
cutOffIndex = nextNewlineIndex + 1;
}
}
text = text.substring(cutOffIndex);
}
if (addPrefix)
text = prefix + text;
return text;
}
/**
* Get the {@code float} from a {@link String}.
*
* @param value The {@link String value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code float} value after parsing the {@link String} value, otherwise
* returns default if failed to read a valid value, like in case of an exception.
*/
public static float getFloatFromString(String value, float def) {
if (value == null) return def;
try {
return Float.parseFloat(value);
}
catch (Exception e) {
return def;
}
}
/**
* Get the {@code int} from a {@link String}.
*
* @param value The {@link String value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code int} value after parsing the {@link String} value, otherwise
* returns default if failed to read a valid value, like in case of an exception.
*/
public static int getIntFromString(String value, int def) {
if (value == null) return def;
try {
return Integer.parseInt(value);
}
catch (Exception e) {
return def;
}
}
/**
* Get an {@code int} from {@link Bundle} that is stored as a {@link String}.
*
* @param bundle The {@link Bundle} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code int} value after parsing the {@link String} value stored in
* {@link Bundle}, otherwise returns default if failed to read a valid value,
* like in case of an exception.
*/
public static int getIntStoredAsStringFromBundle(Bundle bundle, String key, int def) {
if (bundle == null) return def;
return getIntFromString(bundle.getString(key, Integer.toString(def)), def);
}
/**
* If value is not in the range [min, max], set it to either min or max.
*/
public static int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
/**
* If value is not in the range [min, max], set it to default.
*/
public static float rangedOrDefault(float value, float def, float min, float max) {
if (value < min || value > max)
return def;
else
return value;
}
/**
* Get the object itself if it is not {@code null}, otherwise default.
*
* @param object The {@link Object} to check.
* @param def The default {@link Object}.
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
*/
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
return (object == null) ? def : object;
}
public static LinkedHashSet<CharSequence> extractUrls(String text) {
StringBuilder regex_sb = new StringBuilder();
regex_sb.append("("); // Begin first matching group.
regex_sb.append("(?:"); // Begin scheme group.
regex_sb.append("dav|"); // The DAV proto.
regex_sb.append("dict|"); // The DICT proto.
regex_sb.append("dns|"); // The DNS proto.
regex_sb.append("file|"); // File path.
regex_sb.append("finger|"); // The Finger proto.
regex_sb.append("ftp(?:s?)|"); // The FTP proto.
regex_sb.append("git|"); // The Git proto.
regex_sb.append("gopher|"); // The Gopher proto.
regex_sb.append("http(?:s?)|"); // The HTTP proto.
regex_sb.append("imap(?:s?)|"); // The IMAP proto.
regex_sb.append("irc(?:[6s]?)|"); // The IRC proto.
regex_sb.append("ip[fn]s|"); // The IPFS proto.
regex_sb.append("ldap(?:s?)|"); // The LDAP proto.
regex_sb.append("pop3(?:s?)|"); // The POP3 proto.
regex_sb.append("redis(?:s?)|"); // The Redis proto.
regex_sb.append("rsync|"); // The Rsync proto.
regex_sb.append("rtsp(?:[su]?)|"); // The RTSP proto.
regex_sb.append("sftp|"); // The SFTP proto.
regex_sb.append("smb(?:s?)|"); // The SAMBA proto.
regex_sb.append("smtp(?:s?)|"); // The SMTP proto.
regex_sb.append("svn(?:(?:\\+ssh)?)|"); // The Subversion proto.
regex_sb.append("tcp|"); // The TCP proto.
regex_sb.append("telnet|"); // The Telnet proto.
regex_sb.append("tftp|"); // The TFTP proto.
regex_sb.append("udp|"); // The UDP proto.
regex_sb.append("vnc|"); // The VNC proto.
regex_sb.append("ws(?:s?)"); // The Websocket proto.
regex_sb.append(")://"); // End scheme group.
regex_sb.append(")"); // End first matching group.
// Begin second matching group.
regex_sb.append("(");
// User name and/or password in format 'user:pass@'.
regex_sb.append("(?:\\S+(?::\\S*)?@)?");
// Begin host group.
regex_sb.append("(?:");
// IP address (from http://www.regular-expressions.info/examples.html).
regex_sb.append("(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|");
// Host name or domain.
regex_sb.append("(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)(?:(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))?|");
// Just path. Used in case of 'file://' scheme.
regex_sb.append("/(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)");
// End host group.
regex_sb.append(")");
// Port number.
regex_sb.append("(?::\\d{1,5})?");
// Resource path with optional query string.
regex_sb.append("(?:/[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// Fragment.
regex_sb.append("(?:#[a-zA-Z0-9:@%\\-._~!$&()*+,;=?/]*)?");
// End second matching group.
regex_sb.append(")");
final Pattern urlPattern = Pattern.compile(
regex_sb.toString(),
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
Matcher matcher = urlPattern.matcher(text);
while (matcher.find()) {
int matchStart = matcher.start(1);
int matchEnd = matcher.end();
String url = text.substring(matchStart, matchEnd);
urlSet.add(url);
}
return urlSet;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,414 @@
/*
* Copyright (c) 2008, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.termux.shared.file.filesystem;
import android.os.Build;
import android.system.StructStat;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.Set;
import java.util.HashSet;
/**
* Unix implementation of PosixFileAttributes.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileAttributes.java
*/
public class FileAttributes {
private String filePath;
private FileDescriptor fileDescriptor;
private int st_mode;
private long st_ino;
private long st_dev;
private long st_rdev;
private long st_nlink;
private int st_uid;
private int st_gid;
private long st_size;
private long st_blksize;
private long st_blocks;
private long st_atime_sec;
private long st_atime_nsec;
private long st_mtime_sec;
private long st_mtime_nsec;
private long st_ctime_sec;
private long st_ctime_nsec;
// created lazily
private volatile String owner;
private volatile String group;
private volatile FileKey key;
private FileAttributes(String filePath) {
this.filePath = filePath;
}
private FileAttributes(FileDescriptor fileDescriptor) {
this.fileDescriptor = fileDescriptor;
}
// get the FileAttributes for a given file
public static FileAttributes get(String filePath, boolean followLinks) throws IOException {
FileAttributes fileAttributes;
if (filePath == null || filePath.isEmpty())
fileAttributes = new FileAttributes((String) null);
else
fileAttributes = new FileAttributes(new File(filePath).getAbsolutePath());
if (followLinks) {
NativeDispatcher.stat(filePath, fileAttributes);
} else {
NativeDispatcher.lstat(filePath, fileAttributes);
}
// Logger.logDebug(fileAttributes.toString());
return fileAttributes;
}
// get the FileAttributes for an open file
public static FileAttributes get(FileDescriptor fileDescriptor) throws IOException {
FileAttributes fileAttributes = new FileAttributes(fileDescriptor);
NativeDispatcher.fstat(fileDescriptor, fileAttributes);
return fileAttributes;
}
public String file() {
if(filePath != null)
return filePath;
else if(fileDescriptor != null)
return fileDescriptor.toString();
else
return null;
}
// package-private
public boolean isSameFile(FileAttributes attrs) {
return ((st_ino == attrs.st_ino) && (st_dev == attrs.st_dev));
}
// package-private
public int mode() {
return st_mode;
}
public long blksize() {
return st_blksize;
}
public long blocks() {
return st_blocks;
}
public long ino() {
return st_ino;
}
public long dev() {
return st_dev;
}
public long rdev() {
return st_rdev;
}
public long nlink() {
return st_nlink;
}
public int uid() {
return st_uid;
}
public int gid() {
return st_gid;
}
private static FileTime toFileTime(long sec, long nsec) {
if (nsec == 0) {
return FileTime.from(sec, TimeUnit.SECONDS);
} else {
// truncate to microseconds to avoid overflow with timestamps
// way out into the future. We can re-visit this if FileTime
// is updated to define a from(secs,nsecs) method.
long micro = sec * 1000000L + nsec / 1000L;
return FileTime.from(micro, TimeUnit.MICROSECONDS);
}
}
public FileTime lastAccessTime() {
return toFileTime(st_atime_sec, st_atime_nsec);
}
public FileTime lastModifiedTime() {
return toFileTime(st_mtime_sec, st_mtime_nsec);
}
public FileTime lastChangeTime() {
return toFileTime(st_ctime_sec, st_ctime_nsec);
}
public FileTime creationTime() {
return lastModifiedTime();
}
public boolean isRegularFile() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFREG);
}
public boolean isDirectory() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFDIR);
}
public boolean isSymbolicLink() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFLNK);
}
public boolean isCharacter() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFCHR);
}
public boolean isFifo() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFIFO);
}
public boolean isBlock() {
return ((st_mode & UnixConstants.S_IFMT) == UnixConstants.S_IFBLK);
}
public boolean isOther() {
int type = st_mode & UnixConstants.S_IFMT;
return (type != UnixConstants.S_IFREG &&
type != UnixConstants.S_IFDIR &&
type != UnixConstants.S_IFLNK);
}
public boolean isDevice() {
int type = st_mode & UnixConstants.S_IFMT;
return (type == UnixConstants.S_IFCHR ||
type == UnixConstants.S_IFBLK ||
type == UnixConstants.S_IFIFO);
}
public long size() {
return st_size;
}
public FileKey fileKey() {
if (key == null) {
synchronized (this) {
if (key == null) {
key = new FileKey(st_dev, st_ino);
}
}
}
return key;
}
public String owner() {
if (owner == null) {
synchronized (this) {
if (owner == null) {
owner = Integer.toString(st_uid);
}
}
}
return owner;
}
public String group() {
if (group == null) {
synchronized (this) {
if (group == null) {
group = Integer.toString(st_gid);
}
}
}
return group;
}
public Set<FilePermission> permissions() {
int bits = (st_mode & UnixConstants.S_IAMB);
HashSet<FilePermission> perms = new HashSet<>();
if ((bits & UnixConstants.S_IRUSR) > 0)
perms.add(FilePermission.OWNER_READ);
if ((bits & UnixConstants.S_IWUSR) > 0)
perms.add(FilePermission.OWNER_WRITE);
if ((bits & UnixConstants.S_IXUSR) > 0)
perms.add(FilePermission.OWNER_EXECUTE);
if ((bits & UnixConstants.S_IRGRP) > 0)
perms.add(FilePermission.GROUP_READ);
if ((bits & UnixConstants.S_IWGRP) > 0)
perms.add(FilePermission.GROUP_WRITE);
if ((bits & UnixConstants.S_IXGRP) > 0)
perms.add(FilePermission.GROUP_EXECUTE);
if ((bits & UnixConstants.S_IROTH) > 0)
perms.add(FilePermission.OTHERS_READ);
if ((bits & UnixConstants.S_IWOTH) > 0)
perms.add(FilePermission.OTHERS_WRITE);
if ((bits & UnixConstants.S_IXOTH) > 0)
perms.add(FilePermission.OTHERS_EXECUTE);
return perms;
}
public void loadFromStructStat(StructStat structStat) {
this.st_mode = structStat.st_mode;
this.st_ino = structStat.st_ino;
this.st_dev = structStat.st_dev;
this.st_rdev = structStat.st_rdev;
this.st_nlink = structStat.st_nlink;
this.st_uid = structStat.st_uid;
this.st_gid = structStat.st_gid;
this.st_size = structStat.st_size;
this.st_blksize = structStat.st_blksize;
this.st_blocks = structStat.st_blocks;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
this.st_atime_sec = structStat.st_atim.tv_sec;
this.st_atime_nsec = structStat.st_atim.tv_nsec;
this.st_mtime_sec = structStat.st_mtim.tv_sec;
this.st_mtime_nsec = structStat.st_mtim.tv_nsec;
this.st_ctime_sec = structStat.st_ctim.tv_sec;
this.st_ctime_nsec = structStat.st_ctim.tv_nsec;
} else {
this.st_atime_sec = structStat.st_atime;
this.st_atime_nsec = 0;
this.st_mtime_sec = structStat.st_mtime;
this.st_mtime_nsec = 0;
this.st_ctime_sec = structStat.st_ctime;
this.st_ctime_nsec = 0;
}
}
public String getFileString() {
return "File: `" + file() + "`";
}
public String getTypeString() {
return "Type: `" + FileTypes.getFileType(this).getName() + "`";
}
public String getSizeString() {
return "Size: `" + size() + "`";
}
public String getBlocksString() {
return "Blocks: `" + blocks() + "`";
}
public String getIOBlockString() {
return "IO Block: `" + blksize() + "`";
}
public String getDeviceString() {
return "Device: `" + Long.toHexString(st_dev) + "`";
}
public String getInodeString() {
return "Inode: `" + st_ino + "`";
}
public String getLinksString() {
return "Links: `" + nlink() + "`";
}
public String getDeviceTypeString() {
return "Device Type: `" + rdev() + "`";
}
public String getOwnerString() {
return "Owner: `" + owner() + "`";
}
public String getGroupString() {
return "Group: `" + group() + "`";
}
public String getPermissionString() {
return "Permissions: `" + FilePermissions.toString(permissions()) + "`";
}
public String getAccessTimeString() {
return "Access Time: `" + lastAccessTime() + "`";
}
public String getModifiedTimeString() {
return "Modified Time: `" + lastModifiedTime() + "`";
}
public String getChangeTimeString() {
return "Change Time: `" + lastChangeTime() + "`";
}
@NonNull
@Override
public String toString() {
return getFileAttributesLogString(this);
}
public static String getFileAttributesLogString(final FileAttributes fileAttributes) {
if (fileAttributes == null) return "null";
StringBuilder logString = new StringBuilder();
logString.append(fileAttributes.getFileString());
logString.append("\n").append(fileAttributes.getTypeString());
logString.append("\n").append(fileAttributes.getSizeString());
logString.append("\n").append(fileAttributes.getBlocksString());
logString.append("\n").append(fileAttributes.getIOBlockString());
logString.append("\n").append(fileAttributes.getDeviceString());
logString.append("\n").append(fileAttributes.getInodeString());
logString.append("\n").append(fileAttributes.getLinksString());
if(fileAttributes.isBlock() || fileAttributes.isCharacter())
logString.append("\n").append(fileAttributes.getDeviceTypeString());
logString.append("\n").append(fileAttributes.getOwnerString());
logString.append("\n").append(fileAttributes.getGroupString());
logString.append("\n").append(fileAttributes.getPermissionString());
logString.append("\n").append(fileAttributes.getAccessTimeString());
logString.append("\n").append(fileAttributes.getModifiedTimeString());
logString.append("\n").append(fileAttributes.getChangeTimeString());
return logString.toString();
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.termux.shared.file.filesystem;
/**
* Container for device/inode to uniquely identify file.
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixFileKey.java
*/
public class FileKey {
private final long st_dev;
private final long st_ino;
FileKey(long st_dev, long st_ino) {
this.st_dev = st_dev;
this.st_ino = st_ino;
}
@Override
public int hashCode() {
return (int)(st_dev ^ (st_dev >>> 32)) +
(int)(st_ino ^ (st_ino >>> 32));
}
@Override
public boolean equals(Object obj) {
if (obj == this)
return true;
if (!(obj instanceof FileKey))
return false;
FileKey other = (FileKey)obj;
return (this.st_dev == other.st_dev) && (this.st_ino == other.st_ino);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("(dev=")
.append(Long.toHexString(st_dev))
.append(",ino=")
.append(st_ino)
.append(')');
return sb.toString();
}
}

View File

@@ -0,0 +1,88 @@
/*
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.termux.shared.file.filesystem;
/**
* Defines the bits for use with the {@link FileAttributes#permissions()
* permissions} attribute.
*
* <p> The {@link FileAttributes} class defines methods for manipulating
* set of permissions.
*
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermission.java
*
* @since 1.7
*/
public enum FilePermission {
/**
* Read permission, owner.
*/
OWNER_READ,
/**
* Write permission, owner.
*/
OWNER_WRITE,
/**
* Execute/search permission, owner.
*/
OWNER_EXECUTE,
/**
* Read permission, group.
*/
GROUP_READ,
/**
* Write permission, group.
*/
GROUP_WRITE,
/**
* Execute/search permission, group.
*/
GROUP_EXECUTE,
/**
* Read permission, others.
*/
OTHERS_READ,
/**
* Write permission, others.
*/
OTHERS_WRITE,
/**
* Execute/search permission, others.
*/
OTHERS_EXECUTE
}

View File

@@ -0,0 +1,145 @@
/*
* Copyright (c) 2007, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.termux.shared.file.filesystem;
import static com.termux.shared.file.filesystem.FilePermission.*;
import java.util.*;
/**
* This class consists exclusively of static methods that operate on sets of
* {@link FilePermission} objects.
*
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/PosixFilePermissions.java
*
* @since 1.7
*/
public final class FilePermissions {
private FilePermissions() { }
// Write string representation of permission bits to {@code sb}.
private static void writeBits(StringBuilder sb, boolean r, boolean w, boolean x) {
if (r) {
sb.append('r');
} else {
sb.append('-');
}
if (w) {
sb.append('w');
} else {
sb.append('-');
}
if (x) {
sb.append('x');
} else {
sb.append('-');
}
}
/**
* Returns the {@code String} representation of a set of permissions. It
* is guaranteed that the returned {@code String} can be parsed by the
* {@link #fromString} method.
*
* <p> If the set contains {@code null} or elements that are not of type
* {@code FilePermission} then these elements are ignored.
*
* @param perms
* the set of permissions
*
* @return the string representation of the permission set
*/
public static String toString(Set<FilePermission> perms) {
StringBuilder sb = new StringBuilder(9);
writeBits(sb, perms.contains(OWNER_READ), perms.contains(OWNER_WRITE),
perms.contains(OWNER_EXECUTE));
writeBits(sb, perms.contains(GROUP_READ), perms.contains(GROUP_WRITE),
perms.contains(GROUP_EXECUTE));
writeBits(sb, perms.contains(OTHERS_READ), perms.contains(OTHERS_WRITE),
perms.contains(OTHERS_EXECUTE));
return sb.toString();
}
private static boolean isSet(char c, char setValue) {
if (c == setValue)
return true;
if (c == '-')
return false;
throw new IllegalArgumentException("Invalid mode");
}
private static boolean isR(char c) { return isSet(c, 'r'); }
private static boolean isW(char c) { return isSet(c, 'w'); }
private static boolean isX(char c) { return isSet(c, 'x'); }
/**
* Returns the set of permissions corresponding to a given {@code String}
* representation.
*
* <p> The {@code perms} parameter is a {@code String} representing the
* permissions. It has 9 characters that are interpreted as three sets of
* three. The first set refers to the owner's permissions; the next to the
* group permissions and the last to others. Within each set, the first
* character is {@code 'r'} to indicate permission to read, the second
* character is {@code 'w'} to indicate permission to write, and the third
* character is {@code 'x'} for execute permission. Where a permission is
* not set then the corresponding character is set to {@code '-'}.
*
* <p> <b>Usage Example:</b>
* Suppose we require the set of permissions that indicate the owner has read,
* write, and execute permissions, the group has read and execute permissions
* and others have none.
* <pre>
* Set&lt;FilePermission&gt; perms = FilePermissions.fromString("rwxr-x---");
* </pre>
*
* @param perms
* string representing a set of permissions
*
* @return the resulting set of permissions
*
* @throws IllegalArgumentException
* if the string cannot be converted to a set of permissions
*
* @see #toString(Set)
*/
public static Set<FilePermission> fromString(String perms) {
if (perms.length() != 9)
throw new IllegalArgumentException("Invalid mode");
Set<FilePermission> result = EnumSet.noneOf(FilePermission.class);
if (isR(perms.charAt(0))) result.add(OWNER_READ);
if (isW(perms.charAt(1))) result.add(OWNER_WRITE);
if (isX(perms.charAt(2))) result.add(OWNER_EXECUTE);
if (isR(perms.charAt(3))) result.add(GROUP_READ);
if (isW(perms.charAt(4))) result.add(GROUP_WRITE);
if (isX(perms.charAt(5))) result.add(GROUP_EXECUTE);
if (isR(perms.charAt(6))) result.add(OTHERS_READ);
if (isW(perms.charAt(7))) result.add(OTHERS_WRITE);
if (isX(perms.charAt(8))) result.add(OTHERS_EXECUTE);
return result;
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (c) 2009, 2013, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.termux.shared.file.filesystem;
import androidx.annotation.NonNull;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Represents the value of a file's time stamp attribute. For example, it may
* represent the time that the file was last
* {@link FileAttributes#lastModifiedTime() modified},
* {@link FileAttributes#lastAccessTime() accessed},
* or {@link FileAttributes#creationTime() created}.
*
* <p> Instances of this class are immutable.
*
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/nio/file/attribute/FileTime.java
*
* @since 1.7
* @see java.nio.file.Files#setLastModifiedTime
* @see java.nio.file.Files#getLastModifiedTime
*/
public final class FileTime {
/**
* The unit of granularity to interpret the value. Null if
* this {@code FileTime} is converted from an {@code Instant},
* the {@code value} and {@code unit} pair will not be used
* in this scenario.
*/
private final TimeUnit unit;
/**
* The value since the epoch; can be negative.
*/
private final long value;
/**
* The value return by toString (created lazily)
*/
private String valueAsString;
/**
* Initializes a new instance of this class.
*/
private FileTime(long value, TimeUnit unit) {
this.value = value;
this.unit = unit;
}
/**
* Returns a {@code FileTime} representing a value at the given unit of
* granularity.
*
* @param value
* the value since the epoch (1970-01-01T00:00:00Z); can be
* negative
* @param unit
* the unit of granularity to interpret the value
*
* @return a {@code FileTime} representing the given value
*/
public static FileTime from(long value, @NonNull TimeUnit unit) {
Objects.requireNonNull(unit, "unit");
return new FileTime(value, unit);
}
/**
* Returns a {@code FileTime} representing the given value in milliseconds.
*
* @param value
* the value, in milliseconds, since the epoch
* (1970-01-01T00:00:00Z); can be negative
*
* @return a {@code FileTime} representing the given value
*/
public static FileTime fromMillis(long value) {
return new FileTime(value, TimeUnit.MILLISECONDS);
}
/**
* Returns the value at the given unit of granularity.
*
* <p> Conversion from a coarser granularity that would numerically overflow
* saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE}
* if positive.
*
* @param unit
* the unit of granularity for the return value
*
* @return value in the given unit of granularity, since the epoch
* since the epoch (1970-01-01T00:00:00Z); can be negative
*/
public long to(TimeUnit unit) {
Objects.requireNonNull(unit, "unit");
return unit.convert(this.value, this.unit);
}
/**
* Returns the value in milliseconds.
*
* <p> Conversion from a coarser granularity that would numerically overflow
* saturate to {@code Long.MIN_VALUE} if negative or {@code Long.MAX_VALUE}
* if positive.
*
* @return the value in milliseconds, since the epoch (1970-01-01T00:00:00Z)
*/
public long toMillis() {
return unit.toMillis(value);
}
@NonNull
@Override
public String toString() {
return getDate(toMillis(), "yyyy.MM.dd HH:mm:ss.SSS z");
}
public static String getDate(long milliSeconds, String format) {
try {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(milliSeconds);
return new SimpleDateFormat(format).format(calendar.getTime());
} catch(Exception e) {
return Long.toString(milliSeconds);
}
}
}

View File

@@ -0,0 +1,31 @@
package com.termux.shared.file.filesystem;
/** The {@link Enum} that defines file types. */
public enum FileType {
NO_EXIST("no exist", 0), // 0000000
REGULAR("regular", 1), // 0000001
DIRECTORY("directory", 2), // 0000010
SYMLINK("symlink", 4), // 0000100
CHARACTER("character", 8), // 0001000
FIFO("fifo", 16), // 0010000
BLOCK("block", 32), // 0100000
UNKNOWN("unknown", 64); // 1000000
private final String name;
private final int value;
FileType(final String name, final int value) {
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public int getValue() {
return value;
}
}

View File

@@ -0,0 +1,116 @@
package com.termux.shared.file.filesystem;
import android.system.Os;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
import java.io.File;
public class FileTypes {
/** Flags to represent regular, directory and symlink file types defined by {@link FileType} */
public static final int FILE_TYPE_NORMAL_FLAGS = FileType.REGULAR.getValue() | FileType.DIRECTORY.getValue() | FileType.SYMLINK.getValue();
/** Flags to represent any file type defined by {@link FileType} */
public static final int FILE_TYPE_ANY_FLAGS = Integer.MAX_VALUE; // 1111111111111111111111111111111 (31 1's)
public static String convertFileTypeFlagsToNamesString(int fileTypeFlags) {
StringBuilder fileTypeFlagsStringBuilder = new StringBuilder();
FileType[] fileTypes = {FileType.REGULAR, FileType.DIRECTORY, FileType.SYMLINK, FileType.CHARACTER, FileType.FIFO, FileType.BLOCK, FileType.UNKNOWN};
for (FileType fileType : fileTypes) {
if ((fileTypeFlags & fileType.getValue()) > 0)
fileTypeFlagsStringBuilder.append(fileType.getName()).append(",");
}
String fileTypeFlagsString = fileTypeFlagsStringBuilder.toString();
if (fileTypeFlagsString.endsWith(","))
fileTypeFlagsString = fileTypeFlagsString.substring(0, fileTypeFlagsString.lastIndexOf(","));
return fileTypeFlagsString;
}
/**
* Checks the type of file that exists at {@code filePath}.
*
* Returns:
* - {@link FileType#NO_EXIST} if {@code filePath} is {@code null}, empty, an exception is raised
* or no file exists at {@code filePath}.
* - {@link FileType#REGULAR} if file at {@code filePath} is a regular file.
* - {@link FileType#DIRECTORY} if file at {@code filePath} is a directory file.
* - {@link FileType#SYMLINK} if file at {@code filePath} is a symlink file and {@code followLinks} is {@code false}.
* - {@link FileType#CHARACTER} if file at {@code filePath} is a character special file.
* - {@link FileType#FIFO} if file at {@code filePath} is a fifo special file.
* - {@link FileType#BLOCK} if file at {@code filePath} is a block special file.
* - {@link FileType#UNKNOWN} if file at {@code filePath} is of unknown type.
*
* The {@link File#isFile()} and {@link File#isDirectory()} uses {@link Os#stat(String)} system
* call (not {@link Os#lstat(String)}) to check file type and does follow symlinks.
*
* The {@link File#exists()} uses {@link Os#access(String, int)} system call to check if file is
* accessible and does not follow symlinks. However, it returns {@code false} for dangling symlinks,
* on android at least. Check https://stackoverflow.com/a/57747064/14686958
*
* Basically {@link File} API is not reliable to check for symlinks.
*
* So we get the file type directly with {@link Os#lstat(String)} if {@code followLinks} is
* {@code false} and {@link Os#stat(String)} if {@code followLinks} is {@code true}. All exceptions
* are assumed as non-existence.
*
* The {@link org.apache.commons.io.FileUtils#isSymlink(File)} can also be used for checking
* symlinks but {@link FileAttributes} will provide access to more attributes if necessary,
* including getting other special file types considering that {@link File#exists()} can't be
* used to reliably check for non-existence and exclude the other 3 file types. commons.io is
* also not compatible with android < 8 for many things.
*
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/File.java;l=793
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/java/io/UnixFileSystem.java;l=248
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/native/UnixFileSystem_md.c;l=121
* https://cs.android.com/android/_/android/platform/libcore/+/001ac51d61ad7443ba518bf2cf7e086efe698c6d
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Os.java;l=51
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/luni/src/main/java/libcore/io/Libcore.java;l=45
* https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/app/ActivityThread.java;l=7530
*
* @param filePath The {@code path} for file to check.
* @param followLinks The {@code boolean} that decides if symlinks will be followed while
* finding type. If set to {@code true}, then type of symlink target will
* be returned if file at {@code filePath} is a symlink. If set to
* {@code false}, then type of file at {@code filePath} itself will be
* returned.
* @return Returns the {@link FileType} of file.
*/
public static FileType getFileType(final String filePath, final boolean followLinks) {
if (filePath == null || filePath.isEmpty()) return FileType.NO_EXIST;
try {
FileAttributes fileAttributes = FileAttributes.get(filePath, followLinks);
return getFileType(fileAttributes);
} catch (Exception e) {
// If not a ENOENT (No such file or directory) exception
if (!e.getMessage().contains("ENOENT"))
Logger.logError("Failed to get file type for file at path \"" + filePath + "\": " + e.getMessage());
return FileType.NO_EXIST;
}
}
public static FileType getFileType(@NonNull final FileAttributes fileAttributes) {
if (fileAttributes.isRegularFile())
return FileType.REGULAR;
else if (fileAttributes.isDirectory())
return FileType.DIRECTORY;
else if (fileAttributes.isSymbolicLink())
return FileType.SYMLINK;
else if (fileAttributes.isCharacter())
return FileType.CHARACTER;
else if (fileAttributes.isFifo())
return FileType.FIFO;
else if (fileAttributes.isBlock())
return FileType.BLOCK;
else
return FileType.UNKNOWN;
}
}

View File

@@ -0,0 +1,58 @@
package com.termux.shared.file.filesystem;
import android.system.ErrnoException;
import android.system.Os;
import java.io.File;
import java.io.FileDescriptor;
import java.io.IOException;
public class NativeDispatcher {
public static void stat(String filePath, FileAttributes fileAttributes) throws IOException {
validateFileExistence(filePath);
try {
fileAttributes.loadFromStructStat(Os.stat(filePath));
} catch (ErrnoException e) {
throw new IOException("Failed to run Os.stat() on file at path \"" + filePath + "\": " + e.getMessage());
}
}
public static void lstat(String filePath, FileAttributes fileAttributes) throws IOException {
validateFileExistence(filePath);
try {
fileAttributes.loadFromStructStat(Os.lstat(filePath));
} catch (ErrnoException e) {
throw new IOException("Failed to run Os.lstat() on file at path \"" + filePath + "\": " + e.getMessage());
}
}
public static void fstat(FileDescriptor fileDescriptor, FileAttributes fileAttributes) throws IOException {
validateFileDescriptor(fileDescriptor);
try {
fileAttributes.loadFromStructStat(Os.fstat(fileDescriptor));
} catch (ErrnoException e) {
throw new IOException("Failed to run Os.fstat() on file descriptor \"" + fileDescriptor.toString() + "\": " + e.getMessage());
}
}
public static void validateFileExistence(String filePath) throws IOException {
if (filePath == null || filePath.isEmpty()) throw new IOException("The path is null or empty");
File file = new File(filePath);
//if (!file.exists())
// throw new IOException("No such file or directory: \"" + filePath + "\"");
}
public static void validateFileDescriptor(FileDescriptor fileDescriptor) throws IOException {
if (fileDescriptor == null) throw new IOException("The file descriptor is null");
if (!fileDescriptor.valid())
throw new IOException("No such file descriptor: \"" + fileDescriptor.toString() + "\"");
}
}

View File

@@ -0,0 +1,149 @@
/*
* Copyright (c) 2008, 2009, Oracle and/or its affiliates. All rights reserved.
*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*
*/
// AUTOMATICALLY GENERATED FILE - DO NOT EDIT
package com.termux.shared.file.filesystem;
// BEGIN Android-changed: Use constants from android.system.OsConstants. http://b/32203242
// Those constants are initialized by native code to ensure correctness on different architectures.
// AT_SYMLINK_NOFOLLOW (used by fstatat) and AT_REMOVEDIR (used by unlinkat) as of July 2018 do not
// have equivalents in android.system.OsConstants so left unchanged.
import android.system.OsConstants;
/**
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/src/main/java/sun/nio/fs/UnixConstants.java
*/
public class UnixConstants {
private UnixConstants() { }
static final int O_RDONLY = OsConstants.O_RDONLY;
static final int O_WRONLY = OsConstants.O_WRONLY;
static final int O_RDWR = OsConstants.O_RDWR;
static final int O_APPEND = OsConstants.O_APPEND;
static final int O_CREAT = OsConstants.O_CREAT;
static final int O_EXCL = OsConstants.O_EXCL;
static final int O_TRUNC = OsConstants.O_TRUNC;
static final int O_SYNC = OsConstants.O_SYNC;
static final int O_DSYNC = OsConstants.O_DSYNC;
static final int O_NOFOLLOW = OsConstants.O_NOFOLLOW;
static final int S_IAMB = get_S_IAMB();
static final int S_IRUSR = OsConstants.S_IRUSR;
static final int S_IWUSR = OsConstants.S_IWUSR;
static final int S_IXUSR = OsConstants.S_IXUSR;
static final int S_IRGRP = OsConstants.S_IRGRP;
static final int S_IWGRP = OsConstants.S_IWGRP;
static final int S_IXGRP = OsConstants.S_IXGRP;
static final int S_IROTH = OsConstants.S_IROTH;
static final int S_IWOTH = OsConstants.S_IWOTH;
static final int S_IXOTH = OsConstants.S_IXOTH;
static final int S_IFMT = OsConstants.S_IFMT;
static final int S_IFREG = OsConstants.S_IFREG;
static final int S_IFDIR = OsConstants.S_IFDIR;
static final int S_IFLNK = OsConstants.S_IFLNK;
static final int S_IFCHR = OsConstants.S_IFCHR;
static final int S_IFBLK = OsConstants.S_IFBLK;
static final int S_IFIFO = OsConstants.S_IFIFO;
static final int R_OK = OsConstants.R_OK;
static final int W_OK = OsConstants.W_OK;
static final int X_OK = OsConstants.X_OK;
static final int F_OK = OsConstants.F_OK;
static final int ENOENT = OsConstants.ENOENT;
static final int EACCES = OsConstants.EACCES;
static final int EEXIST = OsConstants.EEXIST;
static final int ENOTDIR = OsConstants.ENOTDIR;
static final int EINVAL = OsConstants.EINVAL;
static final int EXDEV = OsConstants.EXDEV;
static final int EISDIR = OsConstants.EISDIR;
static final int ENOTEMPTY = OsConstants.ENOTEMPTY;
static final int ENOSPC = OsConstants.ENOSPC;
static final int EAGAIN = OsConstants.EAGAIN;
static final int ENOSYS = OsConstants.ENOSYS;
static final int ELOOP = OsConstants.ELOOP;
static final int EROFS = OsConstants.EROFS;
static final int ENODATA = OsConstants.ENODATA;
static final int ERANGE = OsConstants.ERANGE;
static final int EMFILE = OsConstants.EMFILE;
// S_IAMB are access mode bits, therefore, calculated by taking OR of all the read, write and
// execute permissions bits for owner, group and other.
private static int get_S_IAMB() {
return (OsConstants.S_IRUSR | OsConstants.S_IWUSR | OsConstants.S_IXUSR |
OsConstants.S_IRGRP | OsConstants.S_IWGRP | OsConstants.S_IXGRP |
OsConstants.S_IROTH | OsConstants.S_IWOTH | OsConstants.S_IXOTH);
}
// END Android-changed: Use constants from android.system.OsConstants. http://b/32203242
static final int AT_SYMLINK_NOFOLLOW = 0x100;
static final int AT_REMOVEDIR = 0x200;
}

View File

@@ -0,0 +1,300 @@
package com.termux.shared.file.tests;
import android.content.Context;
import androidx.annotation.NonNull;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import java.io.File;
import java.nio.charset.Charset;
public class FileUtilsTests {
private static final String LOG_TAG = "FileUtilsTests";
/**
* Run basic tests for {@link FileUtils} class.
*
* Move tests need to be written, specially for failures.
*
* The log level must be set to verbose.
*
* Run at app startup like in an activity
* FileUtilsTests.runTests(this, TermuxConstants.TERMUX_HOME_DIR_PATH + "/FileUtilsTests");
*
* @param context The {@link Context} for operations.
*/
public static void runTests(@NonNull final Context context, @NonNull final String testRootDirectoryPath) {
try {
Logger.logInfo(LOG_TAG, "Running tests");
Logger.logInfo(LOG_TAG, "testRootDirectoryPath: \"" + testRootDirectoryPath + "\"");
String fileUtilsTestsDirectoryCanonicalPath = FileUtils.getCanonicalPath(testRootDirectoryPath, null, false);
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
runTestsInner(context, testRootDirectoryPath);
Logger.logInfo(LOG_TAG, "All tests successful");
} catch (Exception e) {
Logger.logErrorAndShowToast(context, LOG_TAG, e.getMessage());
}
}
private static void runTestsInner(@NonNull final Context context, @NonNull final String testRootDirectoryPath) throws Exception {
String errmsg;
String label;
String path;
/*
* - dir1
* - sub_dir1
* - sub_reg1
* - sub_sym1 (absolute symlink to dir2)
* - sub_sym2 (copy of sub_sym1 for symlink to dir2)
* - sub_sym3 (relative symlink to dir4)
* - dir2
* - sub_reg1
* - sub_reg2 (copy of dir2/sub_reg1)
* - dir3 (copy of dir1)
* - dir4 (moved from dir3)
*/
String dir1_label = "dir1";
String dir1_path = testRootDirectoryPath + "/dir1";
String dir1__sub_dir1_label = "dir1/sub_dir1";
String dir1__sub_dir1_path = dir1_path + "/sub_dir1";
String dir1__sub_reg1_label = "dir1/sub_reg1";
String dir1__sub_reg1_path = dir1_path + "/sub_reg1";
String dir1__sub_sym1_label = "dir1/sub_sym1";
String dir1__sub_sym1_path = dir1_path + "/sub_sym1";
String dir1__sub_sym2_label = "dir1/sub_sym2";
String dir1__sub_sym2_path = dir1_path + "/sub_sym2";
String dir1__sub_sym3_label = "dir1/sub_sym3";
String dir1__sub_sym3_path = dir1_path + "/sub_sym3";
String dir2_label = "dir2";
String dir2_path = testRootDirectoryPath + "/dir2";
String dir2__sub_reg1_label = "dir2/sub_reg1";
String dir2__sub_reg1_path = dir2_path + "/sub_reg1";
String dir2__sub_reg2_label = "dir2/sub_reg2";
String dir2__sub_reg2_path = dir2_path + "/sub_reg2";
String dir3_label = "dir3";
String dir3_path = testRootDirectoryPath + "/dir3";
String dir4_label = "dir4";
String dir4_path = testRootDirectoryPath + "/dir4";
// Create or clear test root directory file
label = "testRootDirectoryPath";
errmsg = FileUtils.clearDirectory(context, label, testRootDirectoryPath);
assertEqual("Failed to create " + label + " directory file", null, errmsg);
if(!FileUtils.directoryFileExists(testRootDirectoryPath, false))
throwException("The " + label + " directory file does not exist as expected after creation");
// Create dir1 directory file
errmsg = FileUtils.createDirectoryFile(context, dir1_label, dir1_path);
assertEqual("Failed to create " + dir1_label + " directory file", null, errmsg);
// Create dir2 directory file
errmsg = FileUtils.createDirectoryFile(context, dir2_label, dir2_path);
assertEqual("Failed to create " + dir2_label + " directory file", null, errmsg);
// Create dir1/sub_dir1 directory file
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
errmsg = FileUtils.createDirectoryFile(context, label, path);
assertEqual("Failed to create " + label + " directory file", null, errmsg);
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after creation");
// Create dir1/sub_reg1 regular file
label = dir1__sub_reg1_label; path = dir1__sub_reg1_path;
errmsg = FileUtils.createRegularFile(context, label, path);
assertEqual("Failed to create " + label + " regular file", null, errmsg);
if(!FileUtils.regularFileExists(path, false))
throwException("The " + label + " regular file does not exist as expected after creation");
// Create dir1/sub_sym1 -> dir2 absolute symlink file
label = dir1__sub_sym1_label; path = dir1__sub_sym1_path;
errmsg = FileUtils.createSymlinkFile(context, label, dir2_path, path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
if(!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after creation");
// Copy dir1/sub_sym1 symlink file to dir1/sub_sym2
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
errmsg = FileUtils.copySymlinkFile(context, label, dir1__sub_sym1_path, path, false);
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, errmsg);
if(!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after copying it from " + dir1__sub_sym1_label);
if(!new File(path).getCanonicalPath().equals(dir2_path))
throwException("The " + label + " symlink file does not point to " + dir2_label);
// Write "line1" to dir2/sub_reg1 regular file
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "line1", false);
assertEqual("Failed to write string to " + label + " file with append mode false", null, errmsg);
if(!FileUtils.regularFileExists(path, false))
throwException("The " + label + " file does not exist as expected after writing to it with append mode false");
// Write "line2" to dir2/sub_reg1 regular file
errmsg = FileUtils.writeStringToFile(context, label, path, Charset.defaultCharset(), "\nline2", true);
assertEqual("Failed to write string to " + label + " file with append mode true", null, errmsg);
// Read dir2/sub_reg1 regular file
StringBuilder dataStringBuilder = new StringBuilder();
errmsg = FileUtils.readStringFromFile(context, label, path, Charset.defaultCharset(), dataStringBuilder, false);
assertEqual("Failed to read from " + label + " file", null, errmsg);
assertEqual("The data read from " + label + " file in not as expected", "line1\nline2", dataStringBuilder.toString());
// Copy dir2/sub_reg1 regular file to dir2/sub_reg2 file
label = dir2__sub_reg2_label; path = dir2__sub_reg2_path;
errmsg = FileUtils.copyRegularFile(context, label, dir2__sub_reg1_path, path, false);
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, errmsg);
if(!FileUtils.regularFileExists(path, false))
throwException("The " + label + " regular file does not exist as expected after copying it from " + dir2__sub_reg1_label);
// Copy dir1 directory file to dir3
label = dir3_label; path = dir3_path;
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
// Copy dir1 directory file to dir3 again to test overwrite
label = dir3_label; path = dir3_path;
errmsg = FileUtils.copyDirectoryFile(context, label, dir2_path, path, false);
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, errmsg);
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir2_label);
// Move dir3 directory file to dir4
label = dir4_label; path = dir4_path;
errmsg = FileUtils.moveDirectoryFile(context, label, dir3_path, path, false);
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, errmsg);
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file does not exist as expected after copying it from " + dir3_label);
// Create dir1/sub_sym3 -> dir4 relative symlink file
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
errmsg = FileUtils.createSymlinkFile(context, label, "../dir4", path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
if(!FileUtils.symlinkFileExists(path))
throwException("The " + label + " symlink file does not exist as expected after creation");
// Create dir1/sub_sym3 -> dirX relative dangling symlink file
// This is to ensure that symlinkFileExists returns true if a symlink file exists but is dangling
label = dir1__sub_sym3_label; path = dir1__sub_sym3_path;
errmsg = FileUtils.createSymlinkFile(context, label, "../dirX", path);
assertEqual("Failed to create " + label + " symlink file", null, errmsg);
if(!FileUtils.symlinkFileExists(path))
throwException("The " + label + " dangling symlink file does not exist as expected after creation");
// Delete dir1/sub_sym2 symlink file
label = dir1__sub_sym2_label; path = dir1__sub_sym2_path;
errmsg = FileUtils.deleteSymlinkFile(context, label, path, false);
assertEqual("Failed to delete " + label + " symlink file", null, errmsg);
if(FileUtils.fileExists(path, false))
throwException("The " + label + " symlink file still exist after deletion");
// Check if dir2 directory file still exists after deletion of dir1/sub_sym2 since it was a symlink to dir2
// When deleting a symlink file, its target must not be deleted
label = dir2_label; path = dir2_path;
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1__sub_sym2_label);
// Delete dir1 directory file
label = dir1_label; path = dir1_path;
errmsg = FileUtils.deleteDirectoryFile(context, label, path, false);
assertEqual("Failed to delete " + label + " directory file", null, errmsg);
if(FileUtils.fileExists(path, false))
throwException("The " + label + " directory file still exist after deletion");
// Check if dir2 directory file and dir2/sub_reg1 regular file still exist after deletion of
// dir1 since there was a dir1/sub_sym1 symlink to dir2 in it
// When deleting a directory, any targets of symlinks must not be deleted when deleting symlink files
label = dir2_label; path = dir2_path;
if(!FileUtils.directoryFileExists(path, false))
throwException("The " + label + " directory file has unexpectedly been deleted after deletion of " + dir1_label);
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
if(!FileUtils.fileExists(path, false))
throwException("The " + label + " regular file has unexpectedly been deleted after deletion of " + dir1_label);
// Delete dir2/sub_reg1 regular file
label = dir2__sub_reg1_label; path = dir2__sub_reg1_path;
errmsg = FileUtils.deleteRegularFile(context, label, path, false);
assertEqual("Failed to delete " + label + " regular file", null, errmsg);
if(FileUtils.fileExists(path, false))
throwException("The " + label + " regular file still exist after deletion");
FileUtils.getFileType("/dev/ptmx", false);
FileUtils.getFileType("/dev/null", false);
}
public static void assertEqual(@NonNull final String message, final String expected, final String actual) throws Exception {
if (!equalsRegardingNull(expected, actual))
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actual + "\"");
}
private static boolean equalsRegardingNull(final String expected, final String actual) {
if (expected == null) {
return actual == null;
}
return isEquals(expected, actual);
}
private static boolean isEquals(String expected, String actual) {
return expected.equals(actual);
}
public static void throwException(@NonNull final String message) throws Exception {
throw new Exception(message);
}
}

View File

@@ -0,0 +1,71 @@
package com.termux.shared.interact;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.text.Selection;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.view.ViewGroup.LayoutParams;
import android.widget.EditText;
import android.widget.LinearLayout;
public final class DialogUtils {
public interface TextSetListener {
void onTextSet(String text);
}
public static void textInput(Activity activity, int titleText, String initialText,
int positiveButtonText, final TextSetListener onPositive,
int neutralButtonText, final TextSetListener onNeutral,
int negativeButtonText, final TextSetListener onNegative,
final DialogInterface.OnDismissListener onDismiss) {
final EditText input = new EditText(activity);
input.setSingleLine();
if (initialText != null) {
input.setText(initialText);
Selection.setSelection(input.getText(), initialText.length());
}
final AlertDialog[] dialogHolder = new AlertDialog[1];
input.setImeActionLabel(activity.getResources().getString(positiveButtonText), KeyEvent.KEYCODE_ENTER);
input.setOnEditorActionListener((v, actionId, event) -> {
onPositive.onTextSet(input.getText().toString());
dialogHolder[0].dismiss();
return true;
});
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, activity.getResources().getDisplayMetrics());
// https://www.google.com/design/spec/components/dialogs.html#dialogs-specs
int paddingTopAndSides = Math.round(16 * dipInPixels);
int paddingBottom = Math.round(24 * dipInPixels);
LinearLayout layout = new LinearLayout(activity);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
layout.setPadding(paddingTopAndSides, paddingTopAndSides, paddingTopAndSides, paddingBottom);
layout.addView(input);
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(titleText).setView(layout)
.setPositiveButton(positiveButtonText, (d, whichButton) -> onPositive.onTextSet(input.getText().toString()));
if (onNeutral != null) {
builder.setNeutralButton(neutralButtonText, (dialog, which) -> onNeutral.onTextSet(input.getText().toString()));
}
if (onNegative == null) {
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setNegativeButton(negativeButtonText, (dialog, which) -> onNegative.onTextSet(input.getText().toString()));
}
if (onDismiss != null) builder.setOnDismissListener(onDismiss);
dialogHolder[0] = builder.create();
dialogHolder[0].setCanceledOnTouchOutside(false);
dialogHolder[0].show();
}
}

View File

@@ -0,0 +1,71 @@
package com.termux.shared.interact;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
public class ShareUtils {
/**
* Open the system app chooser that allows the user to select which app to send the intent.
*
* @param context The context for operations.
* @param intent The intent that describes the choices that should be shown.
* @param title The title for choose menu.
*/
private static void openSystemAppChooser(final Context context, final Intent intent, final String title) {
if(context == null) return;
final Intent chooserIntent = new Intent(Intent.ACTION_CHOOSER);
chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
chooserIntent.putExtra(Intent.EXTRA_TITLE, title);
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(chooserIntent);
}
/**
* Share text.
*
* @param context The context for operations.
* @param subject The subject for sharing.
* @param text The text to share.
*/
public static void shareText(final Context context, final String subject, final String text) {
if(context == null) return;
final Intent shareTextIntent = new Intent(Intent.ACTION_SEND);
shareTextIntent.setType("text/plain");
shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
shareTextIntent.putExtra(Intent.EXTRA_TEXT, DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false));
openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with));
}
/**
* Copy the text to clipboard.
*
* @param context The context for operations.
* @param text The text to copy.
* @param toastString If this is not {@code null} or empty, then a toast is shown if copying to
* clipboard is successful.
*/
public static void copyTextToClipboard(final Context context, final String text, final String toastString) {
if(context == null) return;
final ClipboardManager clipboardManager = ContextCompat.getSystemService(context, ClipboardManager.class);
if (clipboardManager != null) {
clipboardManager.setPrimaryClip(ClipData.newPlainText(null, text));
if (toastString != null && !toastString.isEmpty())
Logger.showToast(context, toastString, true);
}
}
}

View File

@@ -0,0 +1,330 @@
package com.termux.shared.logger;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
import com.termux.shared.R;
import com.termux.shared.termux.TermuxConstants;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.List;
public class Logger {
public static final String DEFAULT_LOG_TAG = TermuxConstants.TERMUX_APP_NAME;
public static final int LOG_LEVEL_OFF = 0; // log nothing
public static final int LOG_LEVEL_NORMAL = 1; // start logging error, warn and info messages and stacktraces
public static final int LOG_LEVEL_DEBUG = 2; // start logging debug messages
public static final int LOG_LEVEL_VERBOSE = 3; // start logging verbose messages
public static final int DEFAULT_LOG_LEVEL = LOG_LEVEL_NORMAL;
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
public static final int LOGGER_ENTRY_SIZE_LIMIT_IN_BYTES = 4 * 1024; // 4KB
public static void logMessage(int logLevel, String tag, String message) {
if(logLevel == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), message);
else if(logLevel == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.w(getFullTag(tag), message);
else if(logLevel == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.i(getFullTag(tag), message);
else if(logLevel == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
Log.d(getFullTag(tag), message);
else if(logLevel == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
Log.v(getFullTag(tag), message);
}
public static void logError(String tag, String message) {
logMessage(Log.ERROR, tag, message);
}
public static void logError(String message) {
logMessage(Log.ERROR, DEFAULT_LOG_TAG, message);
}
public static void logWarn(String tag, String message) {
logMessage(Log.WARN, tag, message);
}
public static void logWarn(String message) {
logMessage(Log.WARN, DEFAULT_LOG_TAG, message);
}
public static void logInfo(String tag, String message) {
logMessage(Log.INFO, tag, message);
}
public static void logInfo(String message) {
logMessage(Log.INFO, DEFAULT_LOG_TAG, message);
}
public static void logDebug(String tag, String message) {
logMessage(Log.DEBUG, tag, message);
}
public static void logDebug(String message) {
logMessage(Log.DEBUG, DEFAULT_LOG_TAG, message);
}
public static void logVerbose(String tag, String message) {
logMessage(Log.VERBOSE, tag, message);
}
public static void logVerbose(String message) {
logMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
}
public static void logErrorAndShowToast(Context context, String tag, String message) {
if (context == null) return;
if(CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL) {
logError(tag, message);
showToast(context, message, true);
}
}
public static void logErrorAndShowToast(Context context, String message) {
logErrorAndShowToast(context, DEFAULT_LOG_TAG, message);
}
public static void logDebugAndShowToast(Context context, String tag, String message) {
if (context == null) return;
if(CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG) {
logDebug(tag, message);
showToast(context, message, true);
}
}
public static void logDebugAndShowToast(Context context, String message) {
logDebugAndShowToast(context, DEFAULT_LOG_TAG, message);
}
public static void logStackTraceWithMessage(String tag, String message, Throwable throwable) {
if(CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), getMessageAndStackTraceString(message, throwable));
}
public static void logStackTraceWithMessage(String message, Throwable throwable) {
logStackTraceWithMessage(DEFAULT_LOG_TAG, message, throwable);
}
public static void logStackTrace(String tag, Throwable throwable) {
logStackTraceWithMessage(tag, null, throwable);
}
public static void logStackTrace(Throwable throwable) {
logStackTraceWithMessage(DEFAULT_LOG_TAG, null, throwable);
}
public static void logStackTracesWithMessage(String tag, String message, List<Throwable> throwableList) {
if(CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
Log.e(getFullTag(tag), getMessageAndStackTracesString(message, throwableList));
}
public static String getMessageAndStackTraceString(String message, Throwable throwable) {
if(message == null && throwable == null)
return null;
else if(message != null && throwable != null)
return message + ":\n" + getStackTraceString(throwable);
else if(throwable == null)
return message;
else
return getStackTraceString(throwable);
}
public static String getMessageAndStackTracesString(String message, List<Throwable> throwableList) {
if(message == null && (throwableList == null || throwableList.size() == 0))
return null;
else if(message != null && (throwableList != null && throwableList.size() != 0))
return message + ":\n" + getStackTracesString(null, getStackTraceStringArray(throwableList));
else if(throwableList == null || throwableList.size() == 0)
return message;
else
return getStackTracesString(null, getStackTraceStringArray(throwableList));
}
public static String getStackTraceString(Throwable throwable) {
if(throwable == null) return null;
String stackTraceString = null;
try {
StringWriter errors = new StringWriter();
PrintWriter pw = new PrintWriter(errors);
throwable.printStackTrace(pw);
pw.close();
stackTraceString = errors.toString();
errors.close();
} catch (IOException e1) {
e1.printStackTrace();
}
return stackTraceString;
}
public static String[] getStackTraceStringArray(Throwable throwable) {
return getStackTraceStringArray(Collections.singletonList(throwable));
}
public static String[] getStackTraceStringArray(List<Throwable> throwableList) {
if (throwableList == null) return null;
final String[] stackTraceStringArray = new String[throwableList.size()];
for (int i = 0; i < throwableList.size(); i++) {
stackTraceStringArray[i] = getStackTraceString(throwableList.get(i));
}
return stackTraceStringArray;
}
public static String getStackTracesString(String label, String[] stackTraceStringArray) {
if(label == null) label = "StackTraces:";
StringBuilder stackTracesString = new StringBuilder(label);
if (stackTraceStringArray == null || stackTraceStringArray.length == 0) {
stackTracesString.append(" -");
} else {
for (int i = 0; i != stackTraceStringArray.length; i++) {
if(stackTraceStringArray.length > 1)
stackTracesString.append("\n\nStacktrace ").append(i + 1);
stackTracesString.append("\n```\n").append(stackTraceStringArray[i]).append("\n```\n");
}
}
return stackTracesString.toString();
}
public static String getStackTracesMarkdownString(String label, String[] stackTraceStringArray) {
if(label == null) label = "StackTraces";
StringBuilder stackTracesString = new StringBuilder("### " + label);
if (stackTraceStringArray == null || stackTraceStringArray.length == 0) {
stackTracesString.append("\n\n`-`");
} else {
for (int i = 0; i != stackTraceStringArray.length; i++) {
if(stackTraceStringArray.length > 1)
stackTracesString.append("\n\n\n#### Stacktrace ").append(i + 1);
stackTracesString.append("\n\n```\n").append(stackTraceStringArray[i]).append("\n```");
}
}
stackTracesString.append("\n##\n");
return stackTracesString.toString();
}
public static String getSingleLineLogStringEntry(String label, Object object, String def) {
if (object != null)
return label + ": `" + object + "`";
else
return label + ": " + def;
}
public static String getMultiLineLogStringEntry(String label, Object object, String def) {
if (object != null)
return label + ":\n```\n" + object + "\n```\n";
else
return label + ": " + def;
}
public static void showToast(final Context context, final String toastText, boolean longDuration) {
if (context == null) return;
new Handler(Looper.getMainLooper()).post(() -> Toast.makeText(context, toastText, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT).show());
}
public static CharSequence[] getLogLevelsArray() {
return new CharSequence[]{
String.valueOf(LOG_LEVEL_OFF),
String.valueOf(LOG_LEVEL_NORMAL),
String.valueOf(LOG_LEVEL_DEBUG),
String.valueOf(LOG_LEVEL_VERBOSE)
};
}
public static CharSequence[] getLogLevelLabelsArray(Context context, CharSequence[] logLevels, boolean addDefaultTag) {
if (logLevels == null) return null;
CharSequence[] logLevelLabels = new CharSequence[logLevels.length];
for(int i=0; i<logLevels.length; i++) {
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
}
return logLevelLabels;
}
public static String getLogLevelLabel(final Context context, final int logLevel, final boolean addDefaultTag) {
String logLabel;
switch (logLevel) {
case LOG_LEVEL_OFF: logLabel = context.getString(R.string.log_level_off); break;
case LOG_LEVEL_NORMAL: logLabel = context.getString(R.string.log_level_normal); break;
case LOG_LEVEL_DEBUG: logLabel = context.getString(R.string.log_level_debug); break;
case LOG_LEVEL_VERBOSE: logLabel = context.getString(R.string.log_level_verbose); break;
default: logLabel = context.getString(R.string.log_level_unknown); break;
}
if (addDefaultTag && logLevel == DEFAULT_LOG_LEVEL)
return logLabel + " (default)";
else
return logLabel;
}
public static int getLogLevel() {
return CURRENT_LOG_LEVEL;
}
public static int setLogLevel(Context context, int logLevel) {
if(logLevel >= LOG_LEVEL_OFF && logLevel <= LOG_LEVEL_VERBOSE)
CURRENT_LOG_LEVEL = logLevel;
else
CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
if(context != null)
showToast(context, context.getString(R.string.log_level_value, getLogLevelLabel(context, CURRENT_LOG_LEVEL, false)),true);
return CURRENT_LOG_LEVEL;
}
public static String getFullTag(String tag) {
if(DEFAULT_LOG_TAG.equals(tag))
return tag;
else
return DEFAULT_LOG_TAG + ":" + tag;
}
}

View File

@@ -0,0 +1,198 @@
package com.termux.shared.markdown;
import android.content.Context;
import android.graphics.Typeface;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.QuoteSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.util.Linkify;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import com.google.common.base.Strings;
import com.termux.shared.R;
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
import org.commonmark.node.BlockQuote;
import org.commonmark.node.Code;
import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.ListItem;
import org.commonmark.node.StrongEmphasis;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin;
import io.noties.markwon.linkify.LinkifyPlugin;
public class MarkdownUtils {
public static final String backtick = "`";
public static final Pattern backticksPattern = Pattern.compile("(" + backtick + "+)");
/**
* Get the markdown code {@link String} for a {@link String}. This ensures all backticks "`" are
* properly escaped so that markdown does not break.
*
* @param string The {@link String} to convert.
* @param codeBlock If the {@link String} is to be converted to a code block or inline code.
* @return Returns the markdown code {@link String}.
*/
public static String getMarkdownCodeForString(String string, boolean codeBlock) {
if(string == null) return null;
if(string.isEmpty()) return "";
int maxConsecutiveBackTicksCount = getMaxConsecutiveBackTicksCount(string);
// markdown requires surrounding backticks count to be at least one more than the count
// of consecutive ticks in the string itself
int backticksCountToUse;
if(codeBlock)
backticksCountToUse = maxConsecutiveBackTicksCount + 3;
else
backticksCountToUse = maxConsecutiveBackTicksCount + 1;
// create a string with n backticks where n==backticksCountToUse
String backticksToUse = Strings.repeat(backtick, backticksCountToUse);
if(codeBlock)
return backticksToUse + "\n" + string + "\n" + backticksToUse;
else {
// add a space to any prefixed or suffixed backtick characters
if(string.startsWith(backtick))
string = " " + string;
if(string.endsWith(backtick))
string = string + " ";
return backticksToUse + string + backticksToUse;
}
}
/**
* Get the max consecutive backticks "`" in a {@link String}.
*
* @param string The {@link String} to check.
* @return Returns the max consecutive backticks count.
*/
public static int getMaxConsecutiveBackTicksCount(String string) {
if(string == null || string.isEmpty()) return 0;
int maxCount = 0;
int matchCount;
Matcher matcher = backticksPattern.matcher(string);
while(matcher.find()) {
matchCount = matcher.group(1).length();
if(matchCount > maxCount)
maxCount = matchCount;
}
return maxCount;
}
public static String getSingleLineMarkdownStringEntry(String label, Object object, String def) {
if (object != null)
return "**" + label + "**: " + getMarkdownCodeForString(object.toString(), false) + " ";
else
return "**" + label + "**: " + def + " ";
}
public static String getMultiLineMarkdownStringEntry(String label, Object object, String def) {
if (object != null)
return "**" + label + "**:\n" + getMarkdownCodeForString(object.toString(), true) + "\n";
else
return "**" + label + "**: " + def + "\n";
}
public static String getLinkMarkdownString(String label, Object object) {
if (object != null)
return "[" + label + "](" + object + ")";
else
return label;
}
/** Check following for more info:
* https://github.com/noties/Markwon/tree/v4.6.2/app-sample
* https://noties.io/Markwon/docs/v4/recycler/
* https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/readme/ReadMeActivity.kt
*/
public static Markwon getRecyclerMarkwonBuilder(Context context) {
return Markwon.builder(context)
.usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES | Linkify.WEB_URLS))
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(FencedCodeBlock.class, (visitor, fencedCodeBlock) -> {
// we actually won't be applying code spans here, as our custom xml view will
// draw background and apply mono typeface
//
// NB the `trim` operation on literal (as code will have a new line at the end)
final CharSequence code = visitor.configuration()
.syntaxHighlight()
.highlight(fencedCodeBlock.getInfo(), fencedCodeBlock.getLiteral().trim());
visitor.builder().append(code);
});
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder
// set color for inline code
.setFactory(Code.class, (configuration, props) -> new Object[]{
new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)),
});
}
})
.build();
}
/** Check following for more info:
* https://github.com/noties/Markwon/tree/v4.6.2/app-sample
* https://github.com/noties/Markwon/blob/v4.6.2/app-sample/src/main/java/io/noties/markwon/app/samples/notification/NotificationSample.java
*/
public static Markwon getSpannedMarkwonBuilder(Context context) {
return Markwon.builder(context)
.usePlugin(StrikethroughPlugin.create())
.usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder
.setFactory(Emphasis.class, (configuration, props) -> new StyleSpan(Typeface.ITALIC))
.setFactory(StrongEmphasis.class, (configuration, props) -> new StyleSpan(Typeface.BOLD))
.setFactory(BlockQuote.class, (configuration, props) -> new QuoteSpan())
.setFactory(Strikethrough.class, (configuration, props) -> new StrikethroughSpan())
// NB! notification does not handle background color
.setFactory(Code.class, (configuration, props) -> new Object[]{
new BackgroundColorSpan(ContextCompat.getColor(context, R.color.background_markdown_code_inline)),
new TypefaceSpan("monospace"),
new AbsoluteSizeSpan(8)
})
// NB! both ordered and bullet list items
.setFactory(ListItem.class, (configuration, props) -> new BulletSpan());
}
})
.build();
}
public static Spanned getSpannedMarkdownText(Context context, String string) {
final Markwon markwon = getSpannedMarkwonBuilder(context);
return markwon.toMarkdown(string);
}
}

View File

@@ -0,0 +1,169 @@
package com.termux.shared.notification;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.os.Build;
import androidx.annotation.Nullable;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
import com.termux.shared.termux.TermuxConstants;
public class NotificationUtils {
/** Do not show notification */
public static final int NOTIFICATION_MODE_NONE = 0;
/** Show notification without sound, vibration or lights */
public static final int NOTIFICATION_MODE_SILENT = 1;
/** Show notification with sound */
public static final int NOTIFICATION_MODE_SOUND = 2;
/** Show notification with vibration */
public static final int NOTIFICATION_MODE_VIBRATE = 3;
/** Show notification with lights */
public static final int NOTIFICATION_MODE_LIGHTS = 4;
/** Show notification with sound and vibration */
public static final int NOTIFICATION_MODE_SOUND_AND_VIBRATE = 5;
/** Show notification with sound and lights */
public static final int NOTIFICATION_MODE_SOUND_AND_LIGHTS = 6;
/** Show notification with vibration and lights */
public static final int NOTIFICATION_MODE_VIBRATE_AND_LIGHTS = 7;
/** Show notification with sound, vibration and lights */
public static final int NOTIFICATION_MODE_ALL = 8;
private static final String LOG_TAG = "NotificationUtils";
/**
* Get the {@link NotificationManager}.
*
* @param context The {@link Context} for operations.
* @return Returns the {@link NotificationManager}.
*/
@Nullable
public static NotificationManager getNotificationManager(final Context context) {
if(context == null) return null;
return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
/**
* Try to get the next unique notification id that isn't already being used by the app.
*
* Termux app and its plugin must use unique notification ids from the same pool due to usage of android:sharedUserId.
* https://commonsware.com/blog/2017/06/07/jobscheduler-job-ids-libraries.html
*
* @param context The {@link Context} for operations.
* @return Returns the notification id that should be safe to use.
*/
public synchronized static int getNextNotificationId(final Context context) {
if(context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
int lastNotificationId = preferences.getLastNotificationId();
int nextNotificationId = lastNotificationId + 1;
while(nextNotificationId == TermuxConstants.TERMUX_APP_NOTIFICATION_ID || nextNotificationId == TermuxConstants.TERMUX_RUN_COMMAND_NOTIFICATION_ID) {
nextNotificationId++;
}
if(nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0)
nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
preferences.setLastNotificationId(nextNotificationId);
return nextNotificationId;
}
/**
* Get {@link Notification.Builder}.
*
* @param context The {@link Context} for operations.
* @param title The title for the notification.
* @param notifiationText The second line text of the notification.
* @param notificationBigText The full text of the notification that may optionally be styled.
* @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked.
* @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}.
* The builder returned will be {@code null} if {@link #NOTIFICATION_MODE_NONE}
* is passed. That case should ideally be handled before calling this function.
* @return Returns the {@link Notification.Builder}.
*/
@Nullable
public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) {
if(context == null) return null;
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle(title);
builder.setContentText(notifiationText);
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
builder.setContentIntent(pendingIntent);
builder.setPriority(priority);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
builder.setChannelId(channelId);
builder = setNotificationDefaults(builder, notificationMode);
return builder;
}
/**
* Setup the notification channel if Android version is greater than or equal to
* {@link Build.VERSION_CODES#O}.
*
* @param context The {@link Context} for operations.
* @param channelId The id of the channel. Must be unique per package.
* @param channelName The user visible name of the channel.
* @param importance The importance of the channel. This controls how interruptive notifications
* posted to this channel are.
*/
public static void setupNotificationChannel(final Context context, final String channelId, final CharSequence channelName, final int importance) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
NotificationManager notificationManager = getNotificationManager(context);
if(notificationManager != null)
notificationManager.createNotificationChannel(channel);
}
public static Notification.Builder setNotificationDefaults(Notification.Builder builder, final int notificationMode) {
// TODO: setDefaults() is deprecated and should also implement setting notification mode via notification channel
switch (notificationMode) {
case NOTIFICATION_MODE_NONE:
Logger.logWarn(LOG_TAG, "The NOTIFICATION_MODE_NONE passed to setNotificationDefaults(), force setting builder to null.");
return null; // return null since notification is not supposed to be shown
case NOTIFICATION_MODE_SILENT:
break;
case NOTIFICATION_MODE_SOUND:
builder.setDefaults(Notification.DEFAULT_SOUND);
break;
case NOTIFICATION_MODE_VIBRATE:
builder.setDefaults(Notification.DEFAULT_VIBRATE);
break;
case NOTIFICATION_MODE_LIGHTS:
builder.setDefaults(Notification.DEFAULT_LIGHTS);
break;
case NOTIFICATION_MODE_SOUND_AND_VIBRATE:
builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE);
break;
case NOTIFICATION_MODE_SOUND_AND_LIGHTS:
builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS);
break;
case NOTIFICATION_MODE_VIBRATE_AND_LIGHTS:
builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS);
break;
case NOTIFICATION_MODE_ALL:
builder.setDefaults(Notification.DEFAULT_ALL);
break;
default:
Logger.logError(LOG_TAG, "Invalid notificationMode: \"" + notificationMode + "\" passed to setNotificationDefaults()");
break;
}
return builder;
}
}

View File

@@ -0,0 +1,110 @@
package com.termux.shared.packages;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import androidx.annotation.NonNull;
import com.termux.shared.logger.Logger;
public class PackageUtils {
/**
* Get the {@link Context} for the package name.
*
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getContextForPackage(@NonNull final Context context, String packageName) {
try {
return context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
} catch (Exception e) {
Logger.logStackTraceWithMessage("Failed to get \"" + packageName + "\" package context.", e);
return null;
}
}
/**
* Get the {@link PackageInfo} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
*/
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (final Exception e) {
return null;
}
}
/**
* Get the app name for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code android:name} attribute.
*/
public static String getAppNameForPackage(@NonNull final Context context) {
return context.getApplicationInfo().loadLabel(context.getPackageManager()).toString();
}
/**
* Get the package name for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the package name.
*/
public static String getPackageNameForPackage(@NonNull final Context context) {
return context.getApplicationInfo().packageName;
}
/**
* Get the {@code targetSdkVersion} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code targetSdkVersion}.
*/
public static int getTargetSDKForPackage(@NonNull final Context context) {
return context.getApplicationInfo().targetSdkVersion;
}
/**
* Get the {@code versionName} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static Boolean isAppForPackageADebugBuild(@NonNull final Context context) {
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
}
/**
* Get the {@code versionCode} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionCode}. This will be {@code null} if an exception is raised.
*/
public static Integer getVersionCodeForPackage(@NonNull final Context context) {
try {
return getPackageInfoForPackage(context).versionCode;
} catch (final Exception e) {
return null;
}
}
/**
* Get the {@code versionName} for the package associated with the {@code context}.
*
* @param context The {@link Context} for the package.
* @return Returns the {@code versionName}. This will be {@code null} if an exception is raised.
*/
public static String getVersionNameForPackage(@NonNull final Context context) {
try {
return getPackageInfoForPackage(context).versionName;
} catch (final Exception e) {
return null;
}
}
}

View File

@@ -0,0 +1,87 @@
package com.termux.shared.packages;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
import androidx.core.content.ContextCompat;
import com.termux.shared.R;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
import java.util.Arrays;
public class PermissionUtils {
public static final int ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE = 0;
private static final String LOG_TAG = "PluginUtils";
public static boolean checkPermissions(Context context, String[] permissions) {
int result;
for (String p:permissions) {
result = ContextCompat.checkSelfPermission(context,p);
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
public static void askPermissions(Activity context, String[] permissions) {
if(context == null || permissions == null) return;
int result;
Logger.showToast(context, context.getString(R.string.message_sudo_please_grant_permissions), true);
try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
for (String permission:permissions) {
result = ContextCompat.checkSelfPermission(context, permission);
if (result != PackageManager.PERMISSION_GRANTED) {
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
context.requestPermissions(new String[]{permission}, 0);
}
}
}
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
boolean permissionGranted;
permissionGranted = Settings.canDrawOverlays(context);
if (!permissionGranted) {
Logger.logWarn(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
return false;
} else {
Logger.logDebug(LOG_TAG, TermuxConstants.TERMUX_APP_NAME + " App already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
return true;
}
}
public static void askDisplayOverOtherAppsPermission(Activity context) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
context.startActivityForResult(intent, ACTION_MANAGE_OVERLAY_PERMISSION_REQUEST_CODE);
}
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
if(!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context);
if(preferences.getPluginErrorNotificationsEnabled())
Logger.showToast(context, context.getString(R.string.error_display_over_other_apps_permission_not_granted), true);
return false;
} else {
return true;
}
}
}

View File

@@ -0,0 +1,395 @@
package com.termux.shared.settings.preferences;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.SharedPreferences;
import com.termux.shared.logger.Logger;
import java.util.Set;
public class SharedPreferenceUtils {
private static final String LOG_TAG = "SharedPreferenceUtils";
/**
* Get {@link SharedPreferences} instance of the preferences file 'name' with the operating mode
* {@link Context#MODE_PRIVATE}. This file will be created in the app package's default
* shared preferences directory.
*
* @param context The {@link Context} to get the {@link SharedPreferences} instance.
* @param name The preferences file basename without extension.
* @return The single {@link SharedPreferences} instance that can be used to retrieve and
* modify the preference values.
*/
public static SharedPreferences getPrivateSharedPreferences(Context context, String name) {
return context.getSharedPreferences(name, Context.MODE_PRIVATE);
}
/**
* Get {@link SharedPreferences} instance of the preferences file 'name' with the operating mode
* {@link Context#MODE_PRIVATE} and {@link Context#MODE_MULTI_PROCESS}. This file will be
* created in the app package's default shared preferences directory.
*
* @param context The {@link Context} to get the {@link SharedPreferences} instance.
* @param name The preferences file basename without extension.
* @return The single {@link SharedPreferences} instance that can be used to retrieve and
* modify the preference values.
*/
public static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context, String name) {
return context.getSharedPreferences(name, Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* Get a {@code boolean} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code boolean} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static boolean getBoolean(SharedPreferences sharedPreferences, String key, boolean def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting boolean value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getBoolean(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting boolean value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set a {@code boolean} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setBoolean(SharedPreferences sharedPreferences, String key, boolean value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting boolean value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putBoolean(key, value).commit();
else
sharedPreferences.edit().putBoolean(key, value).apply();
}
/**
* Get a {@code float} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code float} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static float getFloat(SharedPreferences sharedPreferences, String key, float def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting float value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getFloat(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting float value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set a {@code float} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setFloat(SharedPreferences sharedPreferences, String key, float value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting float value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putFloat(key, value).commit();
else
sharedPreferences.edit().putFloat(key, value).apply();
}
/**
* Get an {@code int} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code int} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static int getInt(SharedPreferences sharedPreferences, String key, int def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting int value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getInt(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting int value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set an {@code int} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setInt(SharedPreferences sharedPreferences, String key, int value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting int value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putInt(key, value).commit();
else
sharedPreferences.edit().putInt(key, value).apply();
}
/**
* Get a {@code long} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code long} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static long getLong(SharedPreferences sharedPreferences, String key, long def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting long value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getLong(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting long value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set a {@code long} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setLong(SharedPreferences sharedPreferences, String key, long value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting long value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putLong(key, value).commit();
else
sharedPreferences.edit().putLong(key, value).apply();
}
/**
* Get a {@code String} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code String} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static String getString(SharedPreferences sharedPreferences, String key, String def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting String value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getString(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting String value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set a {@code String} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setString(SharedPreferences sharedPreferences, String key, String value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting String value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putString(key, value).commit();
else
sharedPreferences.edit().putString(key, value).apply();
}
/**
* Get a {@code Set<String>} from {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code Set<String>} value stored in {@link SharedPreferences}, otherwise returns
* default if failed to read a valid value, like in case of an exception.
*/
public static Set<String> getStringSet(SharedPreferences sharedPreferences, String key, Set<String> def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting Set<String> value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
try {
return sharedPreferences.getStringSet(key, def);
}
catch (ClassCastException e) {
Logger.logStackTraceWithMessage(LOG_TAG, "Error getting Set<String> value for the \"" + key + "\" key from shared preferences. Returning default value \"" + def + "\".", e);
return def;
}
}
/**
* Set a {@code Set<String>} in {@link SharedPreferences}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setStringSet(SharedPreferences sharedPreferences, String key, Set<String> value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting Set<String> value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putStringSet(key, value).commit();
else
sharedPreferences.edit().putStringSet(key, value).apply();
}
/**
* Get an {@code int} from {@link SharedPreferences} that is stored as a {@link String}.
*
* @param sharedPreferences The {@link SharedPreferences} to get the value from.
* @param key The key for the value.
* @param def The default value if failed to read a valid value.
* @return Returns the {@code int} value after parsing the {@link String} value stored in
* {@link SharedPreferences}, otherwise returns default if failed to read a valid value,
* like in case of an exception.
*/
public static int getIntStoredAsString(SharedPreferences sharedPreferences, String key, int def) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Error getting int value for the \"" + key + "\" key from null shared preferences. Returning default value \"" + def + "\".");
return def;
}
String stringValue;
int intValue;
try {
stringValue = sharedPreferences.getString(key, Integer.toString(def));
if(stringValue != null)
intValue = Integer.parseInt(stringValue);
else
intValue = def;
} catch (NumberFormatException | ClassCastException e) {
intValue = def;
}
return intValue;
}
/**
* Set an {@code int} into {@link SharedPreferences} that is stored as a {@link String}.
*
* @param sharedPreferences The {@link SharedPreferences} to set the value in.
* @param key The key for the value.
* @param value The value to store.
* @param commitToFile If set to {@code true}, then value will be set to shared preferences
* in-memory cache and the file synchronously. Ideally, only to be used for
* multi-process use-cases.
*/
@SuppressLint("ApplySharedPref")
public static void setIntStoredAsString(SharedPreferences sharedPreferences, String key, int value, boolean commitToFile) {
if(sharedPreferences == null) {
Logger.logError(LOG_TAG, "Ignoring setting int value \"" + value + "\" for the \"" + key + "\" key into null shared preferences.");
return;
}
if(commitToFile)
sharedPreferences.edit().putString(key, Integer.toString(value)).commit();
else
sharedPreferences.edit().putString(key, Integer.toString(value)).apply();
}
}

View File

@@ -0,0 +1,174 @@
package com.termux.shared.settings.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.TypedValue;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import com.termux.shared.data.DataUtils;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
import javax.annotation.Nonnull;
public class TermuxAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private int MIN_FONTSIZE;
private int MAX_FONTSIZE;
private int DEFAULT_FONTSIZE;
private static final String LOG_TAG = "TermuxAppSharedPreferences";
public TermuxAppSharedPreferences(@Nonnull Context context) {
// We use the default context if failed to get termux package context
mContext = DataUtils.getDefaultIfNull(TermuxUtils.getTermuxPackageContext(context), context);
mSharedPreferences = getPrivateSharedPreferences(mContext);
setFontVariables(context);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public boolean getShowTerminalToolbar() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SHOW_TERMINAL_TOOLBAR, TERMUX_APP.DEFAULT_VALUE_SHOW_TERMINAL_TOOLBAR);
}
public void setShowTerminalToolbar(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_SHOW_TERMINAL_TOOLBAR, value, false);
}
public boolean toogleShowTerminalToolbar() {
boolean currentValue = getShowTerminalToolbar();
setShowTerminalToolbar(!currentValue);
return !currentValue;
}
public boolean getSoftKeyboardEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED, TERMUX_APP.DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED);
}
public void setSoftKeyboardEnabled(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED, value, false);
}
public boolean getKeepScreenOn() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_KEEP_SCREEN_ON, TERMUX_APP.DEFAULT_VALUE_KEEP_SCREEN_ON);
}
public void setKeepScreenOn(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_KEEP_SCREEN_ON, value, false);
}
private void setFontVariables(Context context) {
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
// This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
// to prevent invisible text due to zoom be mistake:
MIN_FONTSIZE = (int) (4f * dipInPixels);
// http://www.google.com/design/spec/style/typography.html#typography-line-height
int defaultFontSize = Math.round(12 * dipInPixels);
// Make it divisible by 2 since that is the minimal adjustment step:
if (defaultFontSize % 2 == 1) defaultFontSize--;
DEFAULT_FONTSIZE = defaultFontSize;
MAX_FONTSIZE = 256;
}
public int getFontSize() {
int fontSize = SharedPreferenceUtils.getIntStoredAsString(mSharedPreferences, TERMUX_APP.KEY_FONTSIZE, DEFAULT_FONTSIZE);
return DataUtils.clamp(fontSize, MIN_FONTSIZE, MAX_FONTSIZE);
}
public void setFontSize(int value) {
SharedPreferenceUtils.setIntStoredAsString(mSharedPreferences, TERMUX_APP.KEY_FONTSIZE, value, false);
}
public void changeFontSize(boolean increase) {
int fontSize = getFontSize();
fontSize += (increase ? 1 : -1) * 2;
fontSize = Math.max(MIN_FONTSIZE, Math.min(fontSize, MAX_FONTSIZE));
setFontSize(fontSize);
}
public String getCurrentSession() {
return SharedPreferenceUtils.getString(mSharedPreferences, TERMUX_APP.KEY_CURRENT_SESSION, null);
}
public void setCurrentSession(String value) {
SharedPreferenceUtils.setString(mSharedPreferences, TERMUX_APP.KEY_CURRENT_SESSION, value, false);
}
public int getLogLevel() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_APP.KEY_LOG_LEVEL, logLevel, false);
}
public int getLastNotificationId() {
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_APP.KEY_LAST_NOTIFICATION_ID, TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID);
}
public void setLastNotificationId(int notificationId) {
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_APP.KEY_LAST_NOTIFICATION_ID, notificationId, false);
}
public boolean getTerminalViewKeyLoggingEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
}
public void setTerminalViewKeyLoggingEnabled(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, value, false);
}
public boolean getPluginErrorNotificationsEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED, TERMUX_APP.DEFAULT_VALUE_PLUGIN_ERROR_NOTIFICATIONS_ENABLED);
}
public void setPluginErrorNotificationsEnabled(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED, value, false);
}
public boolean getCrashReportNotificationsEnabled() {
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED, TERMUX_APP.DEFAULT_VALUE_CRASH_REPORT_NOTIFICATIONS_ENABLED);
}
public void setCrashReportNotificationsEnabled(boolean value) {
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED, value, false);
}
}

View File

@@ -0,0 +1,138 @@
package com.termux.shared.settings.preferences;
/*
* Version: v0.9.0
*
* Changelog
*
* - 0.1.0 (2021-03-12)
* - Initial Release.
*
* - 0.2.0 (2021-03-13)
* - Added `KEY_LOG_LEVEL` and `KEY_TERMINAL_VIEW_LOGGING_ENABLED`.
*
* - 0.3.0 (2021-03-16)
* - Changed to per app scoping of variables so that the same file can store all constants of
* Termux app and its plugins. This will allow {@link com.termux.app.TermuxSettings} to
* manage preferences of plugins as well if they don't have launcher activity themselves
* and also allow plugin apps to make changes to preferences from background.
* - Added following to `TERMUX_TASKER_APP`:
* `KEY_LOG_LEVEL`.
*
* - 0.4.0 (2021-03-13)
* - Added following to `TERMUX_APP`:
* `KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED` and `DEFAULT_VALUE_PLUGIN_ERROR_NOTIFICATIONS_ENABLED`.
*
* - 0.5.0 (2021-03-24)
* - Added following to `TERMUX_APP`:
* `KEY_LAST_NOTIFICATION_ID` and `DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID`.
*
* - 0.6.0 (2021-03-24)
* - Change `DEFAULT_VALUE_KEEP_SCREEN_ON` value to `false` in `TERMUX_APP`.
*
* - 0.7.0 (2021-03-27)
* - Added following to `TERMUX_APP`:
* `KEY_SOFT_KEYBOARD_ENABLED` and `DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED`.
*
* - 0.8.0 (2021-04-06)
* - Added following to `TERMUX_APP`:
* `KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED` and `DEFAULT_VALUE_CRASH_REPORT_NOTIFICATIONS_ENABLED`.
*
* - 0.9.0 (2021-04-07)
* - Updated javadocs.
*/
/**
* A class that defines shared constants of the SharedPreferences used by Termux app and its plugins.
* This class will be hosted by termux-shared lib and should be imported by other termux plugin
* apps as is instead of copying constants to random classes. The 3rd party apps can also import
* it for interacting with termux apps. If changes are made to this file, increment the version number
* and add an entry in the Changelog section above.
*/
public final class TermuxPreferenceConstants {
/**
* Termux app constants.
*/
public static final class TERMUX_APP {
/**
* Defines the key for whether to show terminal toolbar containing extra keys and text input field.
*/
public static final String KEY_SHOW_TERMINAL_TOOLBAR = "show_extra_keys";
public static final boolean DEFAULT_VALUE_SHOW_TERMINAL_TOOLBAR = true;
/**
* Defines the key for whether the soft keyboard will be enabled, for cases where users want
* to use a hardware keyboard instead.
*/
public static final String KEY_SOFT_KEYBOARD_ENABLED = "soft_keyboard_enabled";
public static final boolean DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED = true;
/**
* Defines the key for whether to always keep screen on.
*/
public static final String KEY_KEEP_SCREEN_ON = "screen_always_on";
public static final boolean DEFAULT_VALUE_KEEP_SCREEN_ON = false;
/**
* Defines the key for font size of termux terminal view.
*/
public static final String KEY_FONTSIZE = "fontsize";
/**
* Defines the key for current termux terminal session.
*/
public static final String KEY_CURRENT_SESSION = "current_session";
/**
* Defines the key for current termux log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
/**
* Defines the key for last used notification id.
*/
public static final String KEY_LAST_NOTIFICATION_ID = "last_notification_id";
public static final int DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID = 0;
/**
* Defines the key for whether termux terminal view key logging is enabled or not
*/
public static final String KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED = "terminal_view_key_logging_enabled";
public static final boolean DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED = false;
/**
* Defines the key for whether flashes and notifications for plugin errors are enabled or not.
*/
public static final String KEY_PLUGIN_ERROR_NOTIFICATIONS_ENABLED = "plugin_error_notifications_enabled";
public static final boolean DEFAULT_VALUE_PLUGIN_ERROR_NOTIFICATIONS_ENABLED = true;
/**
* Defines the key for whether notifications for crash reports are enabled or not.
*/
public static final String KEY_CRASH_REPORT_NOTIFICATIONS_ENABLED = "crash_report_notifications_enabled";
public static final boolean DEFAULT_VALUE_CRASH_REPORT_NOTIFICATIONS_ENABLED = true;
}
/**
* Termux Tasker app constants.
*/
public static final class TERMUX_TASKER_APP {
/**
* Defines the key for current termux log level.
*/
public static final String KEY_LOG_LEVEL = "log_level";
}
}

View File

@@ -0,0 +1,52 @@
package com.termux.shared.settings.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_TASKER_APP;
import com.termux.shared.data.DataUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.termux.TermuxUtils;
import javax.annotation.Nonnull;
public class TermuxTaskerAppSharedPreferences {
private final Context mContext;
private final SharedPreferences mSharedPreferences;
private final SharedPreferences mMultiProcessSharedPreferences;
private static final String LOG_TAG = "TermuxTaskerAppSharedPreferences";
public TermuxTaskerAppSharedPreferences(@Nonnull Context context) {
// We use the default context if failed to get termux-tasker package context
mContext = DataUtils.getDefaultIfNull(TermuxUtils.getTermuxTaskerPackageContext(context), context);
mSharedPreferences = getPrivateSharedPreferences(mContext);
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
}
private static SharedPreferences getPrivateSharedPreferences(Context context) {
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
}
public int getLogLevel(boolean readFromFfile) {
if(readFromFfile)
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
else
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
}
public void setLogLevel(Context context, int logLevel, boolean commitToFile) {
logLevel = Logger.setLogLevel(context, logLevel);
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
}
}

View File

@@ -0,0 +1,461 @@
package com.termux.shared.settings.properties;
import android.content.Context;
import android.widget.Toast;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.primitives.Primitives;
import com.termux.shared.logger.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* An implementation similar to android's {@link android.content.SharedPreferences} interface for
* reading and writing to and from ".properties" files which also maintains an in-memory cache for
* the key/value pairs when an instance object is used. Operations are done under
* synchronization locks and should be thread safe.
*
* If {@link SharedProperties} instance object is used, then two types of in-memory cache maps are
* maintained, one for the literal {@link String} values found in the file for the keys and an
* additional one that stores (near) primitive {@link Object} values for internal use by the caller.
*
* The {@link SharedProperties} also provides static functions that can be used to read properties
* from files or individual key values or even their internal values. An automatic mapping to a
* boolean as internal value can also be done. An in-memory cache is not maintained, nor are locks used.
*
* This currently only has read support, write support can/will be added later if needed. Check android's
* SharedPreferencesImpl class for reference implementation.
*
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/app/SharedPreferencesImpl.java
*/
public class SharedProperties {
/**
* The {@link Properties} object that maintains an in-memory cache of values loaded from the
* {@link #mPropertiesFile} file. The key/value pairs are of any keys that are found in the file
* against their literal values in the file.
*/
private Properties mProperties;
/**
* The {@link HashMap<>} object that maintains an in-memory cache of internal values for the values
* loaded from the {@link #mPropertiesFile} file. The key/value pairs are of any keys defined by
* {@link #mPropertiesList} that are found in the file against their internal {@link Object} values
* returned by the call to
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context, String, String)} interface.
*/
private Map<String, Object> mMap;
private final Context mContext;
private final File mPropertiesFile;
private final Set<String> mPropertiesList;
private final SharedPropertiesParser mSharedPropertiesParser;
private final Object mLock = new Object();
/** Defines the bidirectional map for boolean values and their internal values */
public static final ImmutableBiMap<String, Boolean> MAP_GENERIC_BOOLEAN =
new ImmutableBiMap.Builder<String, Boolean>()
.put("true", true)
.put("false", false)
.build();
/** Defines the bidirectional map for inverted boolean values and their internal values */
public static final ImmutableBiMap<String, Boolean> MAP_GENERIC_INVERTED_BOOLEAN =
new ImmutableBiMap.Builder<String, Boolean>()
.put("true", false)
.put("false", true)
.build();
private static final String LOG_TAG = "SharedProperties";
/**
* Constructor for the SharedProperties class.
*
* @param context The Context for operations.
* @param propertiesFile The {@link File} object to load properties from.
* @param propertiesList The {@link Set<String>} object that defined which properties to load.
* If this is set to {@code null}, then all properties that exist in
* {@code propertiesFile} will be read by {@link #loadPropertiesFromDisk()}
* @param sharedPropertiesParser The implementation of the {@link SharedPropertiesParser} interface.
*/
public SharedProperties(@Nonnull Context context, @Nullable File propertiesFile, Set<String> propertiesList, @Nonnull SharedPropertiesParser sharedPropertiesParser) {
mContext = context;
mPropertiesFile = propertiesFile;
mPropertiesList = propertiesList;
mSharedPropertiesParser = sharedPropertiesParser;
mProperties = new Properties();
mMap = new HashMap<>();
}
/**
* Load the properties defined by {@link #mPropertiesList} or all properties if its {@code null}
* from the {@link #mPropertiesFile} file to update the {@link #mProperties} and {@link #mMap}
* in-memory cache.
* Properties are not loading automatically when constructor is called and must be manually called.
*/
public void loadPropertiesFromDisk() {
synchronized (mLock) {
// Get properties from mPropertiesFile
Properties properties = getProperties(false);
// We still need to load default values into mMap, so we assume no properties defined if
// reading from mPropertiesFile failed
if (properties == null)
properties = new Properties();
HashMap<String, Object> map = new HashMap<>();
Properties newProperties = new Properties();
Set<String> propertiesList = mPropertiesList;
if (propertiesList == null)
propertiesList = properties.stringPropertyNames();
String value;
Object internalValue;
for (String key : propertiesList) {
value = properties.getProperty(key); // value will be null if key does not exist in propertiesFile
Logger.logDebug(LOG_TAG, key + " : " + value);
// Call the {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
// interface method to get the internal value to store in the {@link #mMap}.
internalValue = mSharedPropertiesParser.getInternalPropertyValueFromValue(mContext, key, value);
// If the internal value was successfully added to map, then also add value to newProperties
// We only store values in-memory defined by propertiesList
if (putToMap(map, key, internalValue)) { // null internalValue will be put into map
putToProperties(newProperties, key, value); // null value will **not** be into properties
}
}
mMap = map;
mProperties = newProperties;
}
}
/**
* Get the {@link Properties} object for the {@link #mPropertiesFile}. The {@link Properties}
* object will also contain properties not defined by the {@link #mPropertiesList} if cache
* value is {@code false}.
*
* @param cached If {@code true}, then the {@link #mProperties} in-memory cache is returned. Otherwise
* the {@link Properties} object is directly read from the {@link #mPropertiesFile}.
* @return Returns the {@link Properties} object if read from file, otherwise a copy of {@link #mProperties}.
*/
public Properties getProperties(boolean cached) {
synchronized (mLock) {
if (cached) {
if (mProperties == null) mProperties = new Properties();
return getPropertiesCopy(mProperties);
} else {
return getPropertiesFromFile(mContext, mPropertiesFile);
}
}
}
/**
* Get the {@link String} value for the key passed from the {@link #mPropertiesFile}.
*
* @param key The key to read from the {@link Properties} object.
* @param cached If {@code true}, then the value is returned from the {@link #mProperties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the {@link #mPropertiesFile}
* and value is returned from it against the key.
* @return Returns the {@link String} object. This will be {@code null} if key is not found.
*/
public String getProperty(String key, boolean cached) {
synchronized (mLock) {
return (String) getProperties(cached).get(key);
}
}
/**
* Get the {@link #mMap} object for the {@link #mPropertiesFile}. A call to
* {@link #loadPropertiesFromDisk()} must be made before this.
*
* @return Returns a copy of {@link #mMap} object.
*/
public Map<String, Object> getInternalProperties() {
synchronized (mLock) {
if (mMap == null) mMap = new HashMap<>();
return getMapCopy(mMap);
}
}
/**
* Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile}.
* The value is returned from the {@link #mMap} in-memory cache, so a call to
* {@link #loadPropertiesFromDisk()} must be made before this.
*
* @param key The key to read from the {@link #mMap} object.
* @return Returns the {@link Object} object. This will be {@code null} if key is not found or
* if object was {@code null}. Use {@link HashMap#containsKey(Object)} to detect the later.
* situation.
*/
public Object getInternalProperty(String key) {
synchronized (mLock) {
// null keys are not allowed to be stored in mMap
if (key != null)
return getInternalProperties().get(key);
else
return null;
}
}
/**
* A static function to get the {@link Properties} object for the propertiesFile. A lock is not
* taken when this function is called.
*
* @param context The {@link Context} to use to show a flash if an exception is raised while
* reading the file. If context is {@code null}, then flash will not be shown.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @return Returns the {@link Properties} object. It will be {@code null} if an exception is
* raised while reading the file.
*/
public static Properties getPropertiesFromFile(Context context, File propertiesFile) {
Properties properties = new Properties();
if (propertiesFile == null) {
Logger.logWarn(LOG_TAG, "Not loading properties since file is null");
return properties;
}
try {
try (FileInputStream in = new FileInputStream(propertiesFile)) {
Logger.logVerbose(LOG_TAG, "Loading properties from \"" + propertiesFile.getAbsolutePath() + "\" file");
properties.load(new InputStreamReader(in, StandardCharsets.UTF_8));
}
} catch (Exception e) {
if (context != null)
Toast.makeText(context, "Could not open properties file \"" + propertiesFile.getAbsolutePath() + "\": " + e.getMessage(), Toast.LENGTH_LONG).show();
Logger.logStackTraceWithMessage(LOG_TAG, "Error loading properties file \"" + propertiesFile.getAbsolutePath() + "\"", e);
return null;
}
return properties;
}
/**
* A static function to get the {@link String} value for the {@link Properties} key read from
* the propertiesFile file.
*
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)} call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @param def The default value.
* @return Returns the {@link String} object. This will be {@code null} if key is not found.
*/
public static String getProperty(Context context, File propertiesFile, String key, String def) {
return (String) getDefaultIfNull(getDefaultIfNull(getPropertiesFromFile(context, propertiesFile), new Properties()).get(key), def);
}
/**
* A static function to get the internal {@link Object} value for the {@link String} value for
* the {@link Properties} key read from the propertiesFile file.
*
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)} call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @param sharedPropertiesParser The implementation of the {@link SharedPropertiesParser} interface.
* @return Returns the {@link String} Object returned by the call to
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}.
*/
public static Object getInternalProperty(Context context, File propertiesFile, String key, @Nonnull SharedPropertiesParser sharedPropertiesParser) {
String value = (String) getDefaultIfNull(getPropertiesFromFile(context, propertiesFile), new Properties()).get(key);
// Call the {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
// interface method to get the internal value to return.
return sharedPropertiesParser.getInternalPropertyValueFromValue(context, key, value);
}
/**
* A static function to check if the value is {@code true} for {@link Properties} key read from
* the propertiesFile file.
*
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)}call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true",
* regardless of case. If the key does not exist in the file or does not equal "true", then
* {@code false} will be returned.
*/
public static boolean isPropertyValueTrue(Context context, File propertiesFile, String key) {
return (boolean) getBooleanValueForStringValue((String) getProperty(context, propertiesFile, key, null), false);
}
/**
* A static function to check if the value is {@code false} for {@link Properties} key read from
* the propertiesFile file.
*
* @param context The {@link Context} for the {@link #getPropertiesFromFile(Context,File)} call.
* @param propertiesFile The {@link File} to read the {@link Properties} from.
* @param key The key to read.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "false",
* regardless of case. If the key does not exist in the file or does not equal "false", then
* {@code true} will be returned.
*/
public static boolean isPropertyValueFalse(Context context, File propertiesFile, String key) {
return (boolean) getInvertedBooleanValueForStringValue((String) getProperty(context, propertiesFile, key, null), true);
}
/**
* Put a value in a {@link #mMap}.
* The key cannot be {@code null}.
* Only {@code null}, primitive or their wrapper classes or String class objects are allowed to be added to
* the map, although this limitation may be changed.
*
* @param map The {@link Map} object to add value to.
* @param key The key for which to add the value to the map.
* @param value The {@link Object} to add to the map.
* @return Returns {@code true} if value was successfully added, otherwise {@code false}.
*/
public static boolean putToMap(HashMap<String, Object> map, String key, Object value) {
if (map == null) {
Logger.logError(LOG_TAG, "Map passed to SharedProperties.putToProperties() is null");
return false;
}
// null keys are not allowed to be stored in mMap
if (key == null) {
Logger.logError(LOG_TAG, "Cannot put a null key into properties map");
return false;
}
boolean put = false;
if (value != null) {
Class<?> clazz = value.getClass();
if (clazz.isPrimitive() || Primitives.isWrapperType(clazz) || value instanceof String) {
put = true;
}
} else {
put = true;
}
if (put) {
map.put(key, value);
return true;
} else {
Logger.logError(LOG_TAG, "Cannot put a non-primitive value for the key \"" + key + "\" into properties map");
return false;
}
}
/**
* Put a value in a {@link Map}.
* The key cannot be {@code null}.
* Passing {@code null} as the value argument is equivalent to removing the key from the
* properties.
*
* @param properties The {@link Properties} object to add value to.
* @param key The key for which to add the value to the properties.
* @param value The {@link String} to add to the properties.
* @return Returns {@code true} if value was successfully added, otherwise {@code false}.
*/
public static boolean putToProperties(Properties properties, String key, String value) {
if (properties == null) {
Logger.logError(LOG_TAG, "Properties passed to SharedProperties.putToProperties() is null");
return false;
}
// null keys are not allowed to be stored in mMap
if (key == null) {
Logger.logError(LOG_TAG, "Cannot put a null key into properties");
return false;
}
if (value != null) {
properties.put(key, value);
return true;
} else {
properties.remove(key);
}
return true;
}
public static Properties getPropertiesCopy(Properties inputProperties) {
if (inputProperties == null) return null;
Properties outputProperties = new Properties();
for (String key : inputProperties.stringPropertyNames()) {
outputProperties.put(key, inputProperties.get(key));
}
return outputProperties;
}
public static Map<String, Object> getMapCopy(Map<String, Object> map) {
if (map == null) return null;
return new HashMap<>(map);
}
/**
* Get the boolean value for the {@link String} value.
*
* @param value The {@link String} value to convert.
* @param def The default {@link boolean} value to return.
* @return Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively,
* regardless of case. Otherwise returns default value.
*/
public static boolean getBooleanValueForStringValue(String value, boolean def) {
return (boolean) getDefaultIfNull(MAP_GENERIC_BOOLEAN.get(toLowerCase(value)), def);
}
/**
* Get the inverted boolean value for the {@link String} value.
*
* @param value The {@link String} value to convert.
* @param def The default {@link boolean} value to return.
* @return Returns {@code true} or {@code false} if value is the literal string "false" or "true" respectively,
* regardless of case. Otherwise returns default value.
*/
public static boolean getInvertedBooleanValueForStringValue(String value, boolean def) {
return (boolean) getDefaultIfNull(MAP_GENERIC_INVERTED_BOOLEAN.get(toLowerCase(value)), def);
}
/**
* Get the object itself if it is not {@code null}, otherwise default.
*
* @param object The {@link Object} to check.
* @param def The default {@link Object}.
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
*/
public static <T> T getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) {
return (object == null) ? def : object;
}
/**
* Covert the {@link String} value to lowercase.
*
* @param value The {@link String} value to convert.
* @return Returns the lowercased value.
*/
public static String toLowerCase(String value) {
if (value == null) return null; else return value.toLowerCase();
}
}

View File

@@ -0,0 +1,23 @@
package com.termux.shared.settings.properties;
import android.content.Context;
import java.util.HashMap;
/**
* An interface that must be defined by the caller of the {@link SharedProperties} class.
*/
public interface SharedPropertiesParser {
/**
* A function that should return the internal {@link Object} to be stored for a key/value pair
* read from properties file in the {@link HashMap <>} in-memory cache.
*
* @param context The context for operations.
* @param key The key for which the internal object is required.
* @param value The literal value for the property found is the properties file.
* @return Returns the {@link Object} object to store in the {@link HashMap <>} in-memory cache.
*/
Object getInternalPropertyValueFromValue(Context context, String key, String value);
}

View File

@@ -0,0 +1,258 @@
package com.termux.shared.settings.properties;
import com.google.common.collect.ImmutableBiMap;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.logger.Logger;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/*
* Version: v0.6.0
*
* Changelog
*
* - 0.1.0 (2021-03-11)
* - Initial Release.
*
* - 0.2.0 (2021-03-11)
* - Renamed `HOME_PATH` to `TERMUX_HOME_DIR_PATH`.
* - Renamed `TERMUX_PROPERTIES_PRIMARY_PATH` to `TERMUX_PROPERTIES_PRIMARY_FILE_PATH`.
* - Renamed `TERMUX_PROPERTIES_SECONDARY_FILE_PATH` to `TERMUX_PROPERTIES_SECONDARY_FILE_PATH`.
*
* - 0.3.0 (2021-03-16)
* - Add `*TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR*`.
*
* - 0.4.0 (2021-03-16)
* - Removed `MAP_GENERIC_BOOLEAN` and `MAP_GENERIC_INVERTED_BOOLEAN`.
*
* - 0.5.0 (2021-03-25)
* - Add `KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP`.
*
* - 0.6.0 (2021-04-07)
* - Updated javadocs.
*/
/**
* A class that defines shared constants of the SharedProperties used by Termux app and its plugins.
* This class will be hosted by termux-shared lib and should be imported by other termux plugin
* apps as is instead of copying constants to random classes. The 3rd party apps can also import
* it for interacting with termux apps. If changes are made to this file, increment the version number
* and add an entry in the Changelog section above.
*
* The properties are loaded from the first file found at
* {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or
* {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_FILE_PATH}
*/
public final class TermuxPropertyConstants {
/** Defines the key for whether to use back key as the escape key */
public static final String KEY_USE_BACK_KEY_AS_ESCAPE_KEY = "back-key"; // Default: "back-key"
public static final String VALUE_BACK_KEY_BEHAVIOUR_BACK = "back";
public static final String VALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape";
/** Defines the key for whether to enforce character based input to fix the issue where for some devices like Samsung, the letters might not appear until enter is pressed */
public static final String KEY_ENFORCE_CHAR_BASED_INPUT = "enforce-char-based-input"; // Default: "enforce-char-based-input"
/** Defines the key for whether to hide soft keyboard when termux app is started */
public static final String KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP = "hide-soft-keyboard-on-startup"; // Default: "hide-soft-keyboard-on-startup"
/** Defines the key for whether to use black UI */
public static final String KEY_USE_BLACK_UI = "use-black-ui"; // Default: "use-black-ui"
/** Defines the key for whether to use ctrl space workaround to fix the issue where ctrl+space does not work on some ROMs */
public static final String KEY_USE_CTRL_SPACE_WORKAROUND = "ctrl-space-workaround"; // Default: "ctrl-space-workaround"
/** Defines the key for whether to use fullscreen */
public static final String KEY_USE_FULLSCREEN = "fullscreen"; // Default: "fullscreen"
/** Defines the key for whether to use fullscreen workaround */
public static final String KEY_USE_FULLSCREEN_WORKAROUND = "use-fullscreen-workaround"; // Default: "use-fullscreen-workaround"
/** Defines the key for whether virtual volume keys are disabled */
public static final String KEY_VIRTUAL_VOLUME_KEYS_DISABLED = "volume-keys"; // Default: "volume-keys"
public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume";
public static final String VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual";
/** Defines the key for the bell behaviour */
public static final String KEY_BELL_BEHAVIOUR = "bell-character"; // Default: "bell-character"
public static final String VALUE_BELL_BEHAVIOUR_VIBRATE = "vibrate";
public static final String VALUE_BELL_BEHAVIOUR_BEEP = "beep";
public static final String VALUE_BELL_BEHAVIOUR_IGNORE = "ignore";
public static final String DEFAULT_VALUE_BELL_BEHAVIOUR = VALUE_BELL_BEHAVIOUR_VIBRATE;
public static final int IVALUE_BELL_BEHAVIOUR_VIBRATE = 1;
public static final int IVALUE_BELL_BEHAVIOUR_BEEP = 2;
public static final int IVALUE_BELL_BEHAVIOUR_IGNORE = 3;
public static final int DEFAULT_IVALUE_BELL_BEHAVIOUR = IVALUE_BELL_BEHAVIOUR_VIBRATE;
/** Defines the bidirectional map for bell behaviour values and their internal values */
public static final ImmutableBiMap<String, Integer> MAP_BELL_BEHAVIOUR =
new ImmutableBiMap.Builder<String, Integer>()
.put(VALUE_BELL_BEHAVIOUR_VIBRATE, IVALUE_BELL_BEHAVIOUR_VIBRATE)
.put(VALUE_BELL_BEHAVIOUR_BEEP, IVALUE_BELL_BEHAVIOUR_BEEP)
.put(VALUE_BELL_BEHAVIOUR_IGNORE, IVALUE_BELL_BEHAVIOUR_IGNORE)
.build();
/** Defines the key for the bell behaviour */
public static final String KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR = "terminal-toolbar-height"; // Default: "terminal-toolbar-height"
public static final float IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN = 0.4f;
public static final float IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX = 3;
public static final float DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR = 1;
/** Defines the key for create session shortcut */
public static final String KEY_SHORTCUT_CREATE_SESSION = "shortcut.create-session"; // Default: "shortcut.create-session"
/** Defines the key for next session shortcut */
public static final String KEY_SHORTCUT_NEXT_SESSION = "shortcut.next-session"; // Default: "shortcut.next-session"
/** Defines the key for previous session shortcut */
public static final String KEY_SHORTCUT_PREVIOUS_SESSION = "shortcut.previous-session"; // Default: "shortcut.previous-session"
/** Defines the key for rename session shortcut */
public static final String KEY_SHORTCUT_RENAME_SESSION = "shortcut.rename-session"; // Default: "shortcut.rename-session"
public static final int ACTION_SHORTCUT_CREATE_SESSION = 1;
public static final int ACTION_SHORTCUT_NEXT_SESSION = 2;
public static final int ACTION_SHORTCUT_PREVIOUS_SESSION = 3;
public static final int ACTION_SHORTCUT_RENAME_SESSION = 4;
/** Defines the bidirectional map for session shortcut values and their internal actions */
public static final ImmutableBiMap<String, Integer> MAP_SESSION_SHORTCUTS =
new ImmutableBiMap.Builder<String, Integer>()
.put(KEY_SHORTCUT_CREATE_SESSION, ACTION_SHORTCUT_CREATE_SESSION)
.put(KEY_SHORTCUT_NEXT_SESSION, ACTION_SHORTCUT_NEXT_SESSION)
.put(KEY_SHORTCUT_PREVIOUS_SESSION, ACTION_SHORTCUT_PREVIOUS_SESSION)
.put(KEY_SHORTCUT_RENAME_SESSION, ACTION_SHORTCUT_RENAME_SESSION)
.build();
/** Defines the key for the default working directory */
public static final String KEY_DEFAULT_WORKING_DIRECTORY = "default-working-directory"; // Default: "default-working-directory"
/** Defines the default working directory */
public static final String DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY = TermuxConstants.TERMUX_HOME_DIR_PATH;
/** Defines the key for extra keys */
public static final String KEY_EXTRA_KEYS = "extra-keys"; // Default: "extra-keys"
/** Defines the key for extra keys style */
public static final String KEY_EXTRA_KEYS_STYLE = "extra-keys-style"; // Default: "extra-keys-style"
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]";
public static final String DEFAULT_IVALUE_EXTRA_KEYS_STYLE = "default";
/** Defines the set for keys loaded by termux
* Setting this to {@code null} will make {@link SharedProperties} throw an exception.
* */
public static final Set<String> TERMUX_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
// boolean
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
KEY_USE_BACK_KEY_AS_ESCAPE_KEY,
KEY_USE_BLACK_UI,
KEY_USE_CTRL_SPACE_WORKAROUND,
KEY_USE_FULLSCREEN,
KEY_USE_FULLSCREEN_WORKAROUND,
KEY_VIRTUAL_VOLUME_KEYS_DISABLED,
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS,
// int
KEY_BELL_BEHAVIOUR,
// float
KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
// Integer
KEY_SHORTCUT_CREATE_SESSION,
KEY_SHORTCUT_NEXT_SESSION,
KEY_SHORTCUT_PREVIOUS_SESSION,
KEY_SHORTCUT_RENAME_SESSION,
// String
KEY_DEFAULT_WORKING_DIRECTORY,
KEY_EXTRA_KEYS,
KEY_EXTRA_KEYS_STYLE
));
/** Defines the set for keys loaded by termux that have default boolean behaviour
* "true" -> true
* "false" -> false
* default: false
* */
public static final Set<String> TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
KEY_ENFORCE_CHAR_BASED_INPUT,
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
KEY_USE_CTRL_SPACE_WORKAROUND,
KEY_USE_FULLSCREEN,
KEY_USE_FULLSCREEN_WORKAROUND,
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS
));
/** Defines the set for keys loaded by termux that have default inverted boolean behaviour
* "false" -> true
* "true" -> false
* default: true
* */
public static final Set<String> TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
));
/** Returns the first {@link File} found at
* {@link TermuxConstants#TERMUX_PROPERTIES_PRIMARY_FILE_PATH} or
* {@link TermuxConstants#TERMUX_PROPERTIES_SECONDARY_FILE_PATH}
* from which termux properties can be loaded.
* If the {@link File} found is not a regular file or is not readable then null is returned.
*
* @return Returns the {@link File} object for termux properties.
*/
public static File getTermuxPropertiesFile() {
String[] possiblePropertiesFileLocations = {
TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH,
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH
};
File propertiesFile = new File(possiblePropertiesFileLocations[0]);
int i = 0;
while (!propertiesFile.exists() && i < possiblePropertiesFileLocations.length) {
propertiesFile = new File(possiblePropertiesFileLocations[i]);
i += 1;
}
if (propertiesFile.isFile() && propertiesFile.canRead()) {
return propertiesFile;
} else {
Logger.logDebug("No readable termux.properties file found");
return null;
}
}
}

View File

@@ -0,0 +1,458 @@
package com.termux.shared.settings.properties;
import android.content.Context;
import android.content.res.Configuration;
import com.termux.shared.logger.Logger;
import com.termux.shared.data.DataUtils;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.annotation.Nonnull;
public class TermuxSharedProperties implements SharedPropertiesParser {
protected final Context mContext;
protected final SharedProperties mSharedProperties;
protected final File mPropertiesFile;
private static final String LOG_TAG = "TermuxSharedProperties";
public TermuxSharedProperties(@Nonnull Context context) {
mContext = context;
mPropertiesFile = TermuxPropertyConstants.getTermuxPropertiesFile();
mSharedProperties = new SharedProperties(context, mPropertiesFile, TermuxPropertyConstants.TERMUX_PROPERTIES_LIST, this);
loadTermuxPropertiesFromDisk();
}
/**
* Reload the termux properties from disk into an in-memory cache.
*/
public void loadTermuxPropertiesFromDisk() {
mSharedProperties.loadPropertiesFromDisk();
dumpPropertiesToLog();
dumpInternalPropertiesToLog();
}
/**
* Get the {@link Properties} from the {@link #mPropertiesFile} file.
*
* @param cached If {@code true}, then the {@link Properties} in-memory cache is returned.
* Otherwise the {@link Properties} object is read directly from the
* {@link #mPropertiesFile} file.
* @return Returns the {@link Properties} object. It will be {@code null} if an exception is
* raised while reading the file.
*/
public Properties getProperties(boolean cached) {
return mSharedProperties.getProperties(cached);
}
/**
* Get the {@link String} value for the key passed from the {@link #mPropertiesFile} file.
*
* @param key The key to read.
* @param def The default value.
* @param cached If {@code true}, then the value is returned from the the {@link Properties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the file
* and value is returned from it against the key.
* @return Returns the {@link String} object. This will be {@code null} if key is not found.
*/
public String getPropertyValue(String key, String def, boolean cached) {
return SharedProperties.getDefaultIfNull(mSharedProperties.getProperty(key, cached), def);
}
/**
* A function to check if the value is {@code true} for {@link Properties} key read from
* the {@link #mPropertiesFile} file.
*
* @param key The key to read.
* @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the file
* and value is checked from it.
* @return Returns the {@code true} if the {@link Properties} key {@link String} value equals "true",
* regardless of case. If the key does not exist in the file or does not equal "true", then
* {@code false} will be returned.
*/
public boolean isPropertyValueTrue(String key, boolean cached) {
return (boolean) SharedProperties.getBooleanValueForStringValue((String) getPropertyValue(key, null, cached), false);
}
/**
* A function to check if the value is {@code false} for {@link Properties} key read from
* the {@link #mPropertiesFile} file.
*
* @param key The key to read.
* @param cached If {@code true}, then the value is checked from the the {@link Properties} in-memory cache.
* Otherwise the {@link Properties} object is read directly from the file
* and value is checked from it.
* @return Returns {@code true} if the {@link Properties} key {@link String} value equals "false",
* regardless of case. If the key does not exist in the file or does not equal "false", then
* {@code true} will be returned.
*/
public boolean isPropertyValueFalse(String key, boolean cached) {
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue((String) getPropertyValue(key, null, cached), true);
}
/**
* Get the internal value {@link Object} {@link HashMap <>} in-memory cache for the
* {@link #mPropertiesFile} file. A call to {@link #loadTermuxPropertiesFromDisk()} must be made
* before this.
*
* @return Returns a copy of {@link Map} object.
*/
public Map<String, Object> getInternalProperties() {
return mSharedProperties.getInternalProperties();
}
/**
* Get the internal {@link Object} value for the key passed from the {@link #mPropertiesFile} file.
* If cache is {@code true}, then value is returned from the {@link HashMap <>} in-memory cache,
* so a call to {@link #loadTermuxPropertiesFromDisk()} must be made before this.
*
* @param key The key to read from the {@link HashMap<>} in-memory cache.
* @param cached If {@code true}, then the value is returned from the the {@link HashMap <>} in-memory cache,
* but if the value is null, then an attempt is made to return the default value.
* If {@code false}, then the {@link Properties} object is read directly from the file
* and internal value is returned for the property value against the key.
* @return Returns the {@link Object} object. This will be {@code null} if key is not found or
* the object stored against the key is {@code null}.
*/
public Object getInternalPropertyValue(String key, boolean cached) {
Object value;
if (cached) {
value = mSharedProperties.getInternalProperty(key);
// If the value is not null since key was found or if the value was null since the
// object stored for the key was itself null, we detect the later by checking if the key
// exists in the map.
if (value != null || mSharedProperties.getInternalProperties().containsKey(key)) {
return value;
} else {
// This should not happen normally unless mMap was modified after the
// {@link #loadTermuxPropertiesFromDisk()} call
// A null value can still be returned by
// {@link #getInternalPropertyValueFromValue(Context,String,String)} for some keys
value = getInternalPropertyValueFromValue(mContext, key, null);
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cahce, force returning default value: `" + value + "`");
return value;
}
} else {
// We get the property value directly from file and return its internal value
return getInternalPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
}
}
/**
* Override the
* {@link SharedPropertiesParser#getInternalPropertyValueFromValue(Context,String,String)}
* interface function.
*/
@Override
public Object getInternalPropertyValueFromValue(Context context, String key, String value) {
return getInternalTermuxPropertyValueFromValue(context, key, value);
}
/**
* A static function that should return the internal termux {@link Object} for a key/value pair
* read from properties file.
*
* @param context The context for operations.
* @param key The key for which the internal object is required.
* @param value The literal value for the property found is the properties file.
* @return Returns the internal termux {@link Object} object.
*/
public static Object getInternalTermuxPropertyValueFromValue(Context context, String key, String value) {
if (key == null) return null;
/*
For keys where a MAP_* is checked by respective functions. Note that value to this function
would actually be the key for the MAP_*:
- If the value is currently null, then searching MAP_* should also return null and internal default value will be used.
- If the value is not null and does not exist in MAP_*, then internal default value will be used.
- If the value is not null and does exist in MAP_*, then internal value returned by map will be used.
*/
switch (key) {
// boolean
case TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY:
return (boolean) getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_USE_BLACK_UI:
return (boolean) getUseBlackUIInternalPropertyValueFromValue(context, value);
case TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED:
return (boolean) getVolumeKeysDisabledInternalPropertyValueFromValue(value);
// int
case TermuxPropertyConstants.KEY_BELL_BEHAVIOUR:
return (int) getBellBehaviourInternalPropertyValueFromValue(value);
// float
case TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR:
return (float) getTerminalToolbarHeightScaleFactorInternalPropertyValueFromValue(value);
// Integer (may be null)
case TermuxPropertyConstants.KEY_SHORTCUT_CREATE_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_NEXT_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_PREVIOUS_SESSION:
case TermuxPropertyConstants.KEY_SHORTCUT_RENAME_SESSION:
return (Integer) getCodePointForSessionShortcuts(key, value);
// String (may be null)
case TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY:
return (String) getDefaultWorkingDirectoryInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_EXTRA_KEYS:
return (String) getExtraKeysInternalPropertyValueFromValue(value);
case TermuxPropertyConstants.KEY_EXTRA_KEYS_STYLE:
return (String) getExtraKeysStyleInternalPropertyValueFromValue(value);
default:
// default boolean behaviour
if (TermuxPropertyConstants.TERMUX_DEFAULT_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
return (boolean) SharedProperties.getBooleanValueForStringValue(value, false);
// default inverted boolean behaviour
else if (TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(value, true);
// just use String object as is (may be null)
else
return value;
}
}
/**
* Returns {@code true} if value is not {@code null} and equals {@link TermuxPropertyConstants#VALUE_BACK_KEY_BEHAVIOUR_ESCAPE}, otherwise false.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static boolean getUseBackKeyAsEscapeKeyInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_BACK).equals(TermuxPropertyConstants.VALUE_BACK_KEY_BEHAVIOUR_ESCAPE);
}
/**
* Returns {@code true} or {@code false} if value is the literal string "true" or "false" respectively regardless of case.
* Otherwise returns {@code true} if the night mode is currently enabled in the system.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static boolean getUseBlackUIInternalPropertyValueFromValue(Context context, String value) {
int nightMode = context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
return SharedProperties.getBooleanValueForStringValue(value, nightMode == Configuration.UI_MODE_NIGHT_YES);
}
/**
* Returns {@code true} if value is not {@code null} and equals
* {@link TermuxPropertyConstants#VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME}, otherwise {@code false}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static boolean getVolumeKeysDisabledInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL).equals(TermuxPropertyConstants.VALUE_VOLUME_KEY_BEHAVIOUR_VOLUME);
}
/**
* Returns the internal value after mapping it based on
* {@code TermuxPropertyConstants#MAP_BELL_BEHAVIOUR} if the value is not {@code null}
* and is valid, otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_BELL_BEHAVIOUR}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static int getBellBehaviourInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(TermuxPropertyConstants.MAP_BELL_BEHAVIOUR.get(SharedProperties.toLowerCase(value)), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR);
}
/**
* Returns the int for the value if its not null and is between
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static float getTerminalToolbarHeightScaleFactorInternalPropertyValueFromValue(String value) {
return rangeTerminalToolbarHeightScaleFactorValue(DataUtils.getFloatFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR));
}
/**
* Returns the value itself if it is between
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
* {@code TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
* otherwise returns {@code TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR}.
*
* @param value The value to clamp.
* @return Returns the clamped value.
*/
public static float rangeTerminalToolbarHeightScaleFactorValue(float value) {
return DataUtils.rangedOrDefault(value,
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN,
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX);
}
/**
* Returns the code point for the value if key is not {@code null} and value is not {@code null} and is valid,
* otherwise returns {@code null}.
*
* @param key The key for session shortcut.
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static Integer getCodePointForSessionShortcuts(String key, String value) {
if (key == null) return null;
if (value == null) return null;
String[] parts = value.toLowerCase().trim().split("\\+");
String input = parts.length == 2 ? parts[1].trim() : null;
if (!(parts.length == 2 && parts[0].trim().equals("ctrl")) || input.isEmpty() || input.length() > 2) {
Logger.logError(LOG_TAG, "Keyboard shortcut '" + key + "' is not Ctrl+<something>");
return null;
}
char c = input.charAt(0);
int codePoint = c;
if (Character.isLowSurrogate(c)) {
if (input.length() != 2 || Character.isHighSurrogate(input.charAt(1))) {
Logger.logError(LOG_TAG, "Keyboard shortcut '" + key + "' is not Ctrl+<something>");
return null;
} else {
codePoint = Character.toCodePoint(input.charAt(1), c);
}
}
return codePoint;
}
/**
* Returns the path itself if a directory exists at it and is readable, otherwise returns
* {@link TermuxPropertyConstants#DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY}.
*
* @param path The {@link String} path to check.
* @return Returns the internal value for value.
*/
public static String getDefaultWorkingDirectoryInternalPropertyValueFromValue(String path) {
if (path == null || path.isEmpty()) return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY;
File workDir = new File(path);
if (!workDir.exists() || !workDir.isDirectory() || !workDir.canRead()) {
// Fallback to default directory if user configured working directory does not exist
// or is not a directory or is not readable.
return TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY;
} else {
return path;
}
}
/**
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS}.
*
* @param value The {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static String getExtraKeysInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS);
}
/**
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_EXTRA_KEYS_STYLE}.
*
* @param value {@link String} value to convert.
* @return Returns the internal value for value.
*/
public static String getExtraKeysStyleInternalPropertyValueFromValue(String value) {
return SharedProperties.getDefaultIfNull(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
}
public boolean isEnforcingCharBasedInput() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
}
public boolean shouldSoftKeyboardBeHiddenOnStartup() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
}
public boolean isBackKeyTheEscapeKey() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BACK_KEY_AS_ESCAPE_KEY, true);
}
public boolean isUsingBlackUI() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_BLACK_UI, true);
}
public boolean isUsingCtrlSpaceWorkaround() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_CTRL_SPACE_WORKAROUND, true);
}
public boolean isUsingFullScreen() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN, true);
}
public boolean isUsingFullScreenWorkAround() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_USE_FULLSCREEN_WORKAROUND, true);
}
public boolean areVirtualVolumeKeysDisabled() {
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_VIRTUAL_VOLUME_KEYS_DISABLED, true);
}
public int getBellBehaviour() {
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, true);
}
public float getTerminalToolbarHeightScaleFactor() {
return rangeTerminalToolbarHeightScaleFactorValue((float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true));
}
public String getDefaultWorkingDirectory() {
return (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY, true);
}
public void dumpPropertiesToLog() {
Properties properties = getProperties(true);
StringBuilder propertiesDump = new StringBuilder();
propertiesDump.append("Termux Properties:");
if (properties != null) {
for (String key : properties.stringPropertyNames()) {
propertiesDump.append("\n").append(key).append(": `").append(properties.get(key)).append("`");
}
} else {
propertiesDump.append(" null");
}
Logger.logVerbose(LOG_TAG, propertiesDump.toString());
}
public void dumpInternalPropertiesToLog() {
HashMap<String, Object> internalProperties = (HashMap<String, Object>) getInternalProperties();
StringBuilder internalPropertiesDump = new StringBuilder();
internalPropertiesDump.append("Termux Internal Properties:");
if (internalProperties != null) {
for (String key : internalProperties.keySet()) {
internalPropertiesDump.append("\n").append(key).append(": `").append(internalProperties.get(key)).append("`");
}
}
Logger.logVerbose(LOG_TAG, internalPropertiesDump.toString());
}
}

View File

@@ -0,0 +1,156 @@
package com.termux.shared.shell;
import android.content.Context;
import com.termux.shared.termux.TermuxConstants;
import com.termux.shared.file.FileUtils;
import com.termux.shared.logger.Logger;
import com.termux.shared.packages.PackageUtils;
import com.termux.shared.termux.TermuxUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ShellUtils {
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH;
List<String> environment = new ArrayList<>();
// This function may be called by a different package like a plugin, so we get version for Termux package via its context
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
if(termuxPackageContext != null) {
String termuxVersionName = PackageUtils.getVersionNameForPackage(termuxPackageContext);
if(termuxVersionName != null)
environment.add("TERMUX_VERSION=" + termuxVersionName);
}
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
environment.add("BOOTCLASSPATH=" + System.getenv("BOOTCLASSPATH"));
environment.add("ANDROID_ROOT=" + System.getenv("ANDROID_ROOT"));
environment.add("ANDROID_DATA=" + System.getenv("ANDROID_DATA"));
// EXTERNAL_STORAGE is needed for /system/bin/am to work on at least
// Samsung S7 - see https://plus.google.com/110070148244138185604/posts/gp8Lk3aCGp3.
environment.add("EXTERNAL_STORAGE=" + System.getenv("EXTERNAL_STORAGE"));
// These variables are needed if running on Android 10 and higher.
addToEnvIfPresent(environment, "ANDROID_ART_ROOT");
addToEnvIfPresent(environment, "DEX2OATBOOTCLASSPATH");
addToEnvIfPresent(environment, "ANDROID_I18N_ROOT");
addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT");
addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT");
if (isFailSafe) {
// Keep the default path so that system binaries can be used in the failsafe session.
environment.add("PATH= " + System.getenv("PATH"));
} else {
environment.add("LANG=en_US.UTF-8");
environment.add("PATH=" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH);
environment.add("PWD=" + workingDirectory);
environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH);
}
return environment.toArray(new String[0]);
}
public static void addToEnvIfPresent(List<String> environment, String name) {
String value = System.getenv(name);
if (value != null) {
environment.add(name + "=" + value);
}
}
public static int getPid(Process p) {
try {
Field f = p.getClass().getDeclaredField("pid");
f.setAccessible(true);
try {
return f.getInt(p);
} finally {
f.setAccessible(false);
}
} catch (Throwable e) {
return -1;
}
}
public static String[] setupProcessArgs(String fileToExecute, String[] arguments) {
// The file to execute may either be:
// - An elf file, in which we execute it directly.
// - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the
// system /system/bin/sh. The system shell may vary and may not work at all due to LD_LIBRARY_PATH.
// - A file with shebang, which we try to handle with e.g. /bin/foo -> $PREFIX/bin/foo.
String interpreter = null;
try {
File file = new File(fileToExecute);
try (FileInputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[256];
int bytesRead = in.read(buffer);
if (bytesRead > 4) {
if (buffer[0] == 0x7F && buffer[1] == 'E' && buffer[2] == 'L' && buffer[3] == 'F') {
// Elf file, do nothing.
} else if (buffer[0] == '#' && buffer[1] == '!') {
// Try to parse shebang.
StringBuilder builder = new StringBuilder();
for (int i = 2; i < bytesRead; i++) {
char c = (char) buffer[i];
if (c == ' ' || c == '\n') {
if (builder.length() == 0) {
// Skip whitespace after shebang.
} else {
// End of shebang.
String executable = builder.toString();
if (executable.startsWith("/usr") || executable.startsWith("/bin")) {
String[] parts = executable.split("/");
String binary = parts[parts.length - 1];
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/" + binary;
}
break;
}
} else {
builder.append(c);
}
}
} else {
// No shebang and no ELF, use standard shell.
interpreter = TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/sh";
}
}
}
} catch (IOException e) {
// Ignore.
}
List<String> result = new ArrayList<>();
if (interpreter != null) result.add(interpreter);
result.add(fileToExecute);
if (arguments != null) Collections.addAll(result, arguments);
return result.toArray(new String[0]);
}
public static String getExecutableBasename(String executable) {
if(executable == null) return null;
int lastSlash = executable.lastIndexOf('/');
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
}
public static void clearTermuxTMPDIR(Context context) {
String errmsg;
errmsg = FileUtils.clearDirectory(context, "$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null, false));
if (errmsg != null) {
Logger.logErrorAndShowToast(context, errmsg);
}
}
}

View File

@@ -0,0 +1,298 @@
/*
* Copyright (C) 2012-2019 Jorrit "Chainfire" Jongma
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.termux.shared.shell;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Locale;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.termux.shared.logger.Logger;
/**
* Thread utility class continuously reading from an InputStream
*/
@SuppressWarnings({"WeakerAccess"})
public class StreamGobbler extends Thread {
private static int threadCounter = 0;
private static int incThreadCounter() {
synchronized (StreamGobbler.class) {
int ret = threadCounter;
threadCounter++;
return ret;
}
}
/**
* Line callback interface
*/
public interface OnLineListener {
/**
* <p>Line callback</p>
*
* <p>This callback should process the line as quickly as possible.
* Delays in this callback may pause the native process or even
* result in a deadlock</p>
*
* @param line String that was gobbled
*/
void onLine(String line);
}
/**
* Stream closed callback interface
*/
public interface OnStreamClosedListener {
/**
* <p>Stream closed callback</p>
*/
void onStreamClosed();
}
@NonNull
private final String shell;
@NonNull
private final InputStream inputStream;
@NonNull
private final BufferedReader reader;
@Nullable
private final List<String> listWriter;
@Nullable
private final StringBuilder stringWriter;
@Nullable
private final OnLineListener lineListener;
@Nullable
private final OnStreamClosedListener streamClosedListener;
private volatile boolean active = true;
private volatile boolean calledOnClose = false;
private static final String LOG_TAG = "StreamGobbler";
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputList {@literal List<String>} to write to, or null
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable List<String> outputList) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = null;
listWriter = outputList;
stringWriter = null;
lineListener = null;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
* Do not use this for concurrent reading for STDOUT and STDERR for the same StringBuilder since
* its not synchronized.
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param outputString {@literal List<String>} to write to, or null
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable StringBuilder outputString) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = null;
listWriter = null;
stringWriter = outputString;
lineListener = null;
}
/**
* <p>StreamGobbler constructor</p>
*
* <p>We use this class because shell STDOUT and STDERR should be read as quickly as
* possible to prevent a deadlock from occurring, or Process.waitFor() never
* returning (as the buffer is full, pausing the native process)</p>
*
* @param shell Name of the shell
* @param inputStream InputStream to read from
* @param onLineListener OnLineListener callback
* @param onStreamClosedListener OnStreamClosedListener callback
*/
@AnyThread
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream, @Nullable OnLineListener onLineListener, @Nullable OnStreamClosedListener onStreamClosedListener) {
super("Gobbler#" + incThreadCounter());
this.shell = shell;
this.inputStream = inputStream;
reader = new BufferedReader(new InputStreamReader(inputStream));
streamClosedListener = onStreamClosedListener;
listWriter = null;
stringWriter = null;
lineListener = onLineListener;
}
@Override
public void run() {
// keep reading the InputStream until it ends (or an error occurs)
// optionally pausing when a command is executed that consumes the InputStream itself
int logLevel = Logger.getLogLevel();
try {
String line;
while ((line = reader.readLine()) != null) {
if(logLevel >= Logger.LOG_LEVEL_VERBOSE)
Logger.logVerbose(LOG_TAG, String.format(Locale.ENGLISH, "[%s] %s", shell, line)); // This will get truncated by LOGGER_ENTRY_MAX_LEN, likely 4KB
if (stringWriter != null) stringWriter.append(line).append("\n");
if (listWriter != null) listWriter.add(line);
if (lineListener != null) lineListener.onLine(line);
while (!active) {
synchronized (this) {
try {
this.wait(128);
} catch (InterruptedException e) {
// no action
}
}
}
}
} catch (IOException e) {
// reader probably closed, expected exit condition
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
}
// make sure our stream is closed and resources will be freed
try {
reader.close();
} catch (IOException e) {
// read already closed
}
if (!calledOnClose) {
if (streamClosedListener != null) {
calledOnClose = true;
streamClosedListener.onStreamClosed();
}
}
}
/**
* <p>Resume consuming the input from the stream</p>
*/
@AnyThread
public void resumeGobbling() {
if (!active) {
synchronized (this) {
active = true;
this.notifyAll();
}
}
}
/**
* <p>Suspend gobbling, so other code may read from the InputStream instead</p>
*
* <p>This should <i>only</i> be called from the OnLineListener callback!</p>
*/
@AnyThread
public void suspendGobbling() {
synchronized (this) {
active = false;
this.notifyAll();
}
}
/**
* <p>Wait for gobbling to be suspended</p>
*
* <p>Obviously this cannot be called from the same thread as {@link #suspendGobbling()}</p>
*/
@WorkerThread
public void waitForSuspend() {
synchronized (this) {
while (active) {
try {
this.wait(32);
} catch (InterruptedException e) {
// no action
}
}
}
}
/**
* <p>Is gobbling suspended ?</p>
*
* @return is gobbling suspended?
*/
@AnyThread
public boolean isSuspended() {
synchronized (this) {
return !active;
}
}
/**
* <p>Get current source InputStream</p>
*
* @return source InputStream
*/
@NonNull
@AnyThread
public InputStream getInputStream() {
return inputStream;
}
/**
* <p>Get current OnLineListener</p>
*
* @return OnLineListener
*/
@Nullable
@AnyThread
public OnLineListener getOnLineListener() {
return lineListener;
}
void conditionalJoin() throws InterruptedException {
if (calledOnClose) return; // deadlock from callback, we're inside exit procedure
if (Thread.currentThread() == this) return; // can't join self
join();
}
}

View File

@@ -0,0 +1,796 @@
package com.termux.shared.termux;
import android.annotation.SuppressLint;
import java.io.File;
/*
* Version: v0.17.0
*
* Changelog
*
* - 0.1.0 (2021-03-08)
* - Initial Release.
*
* - 0.2.0 (2021-03-11)
* - Added `_DIR` and `_FILE` substrings to paths.
* - Added `INTERNAL_PRIVATE_APP_DATA_DIR*`, `TERMUX_CACHE_DIR*`, `TERMUX_DATABASES_DIR*`,
* `TERMUX_SHARED_PREFERENCES_DIR*`, `TERMUX_BIN_PREFIX_DIR*`, `TERMUX_ETC_DIR*`,
* `TERMUX_INCLUDE_DIR*`, `TERMUX_LIB_DIR*`, `TERMUX_LIBEXEC_DIR*`, `TERMUX_SHARE_DIR*`,
* `TERMUX_TMP_DIR*`, `TERMUX_VAR_DIR*`, `TERMUX_STAGING_PREFIX_DIR*`,
* `TERMUX_STORAGE_HOME_DIR*`, `TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME*`,
* `TERMUX_DEFAULT_PREFERENCES_FILE`.
* - Renamed `DATA_HOME_PATH` to `TERMUX_DATA_HOME_DIR_PATH`.
* - Renamed `CONFIG_HOME_PATH` to `TERMUX_CONFIG_HOME_DIR_PATH`.
* - Updated javadocs and spacing.
*
* - 0.3.0 (2021-03-12)
* - Remove `TERMUX_CACHE_DIR_PATH*`, `TERMUX_DATABASES_DIR_PATH*`,
* `TERMUX_SHARED_PREFERENCES_DIR_PATH*` since they may not be consistent on all devices.
* - Renamed `TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME` to
* `TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`. This should be used for
* accessing shared preferences between Termux app and its plugins if ever needed by first
* getting shared package context with {@link Context.createPackageContext(String,int}).
*
* - 0.4.0 (2021-03-16)
* - Added `BROADCAST_TERMUX_OPENED`,
* `TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`
* `TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`,
* `TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`,
* `TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`,
* `TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`,
* `TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION`.
*
* - 0.5.0 (2021-03-16)
* - Renamed "Termux Plugin app" labels to "Termux Tasker app".
*
* - 0.6.0 (2021-03-16)
* - Added `TERMUX_FILE_SHARE_URI_AUTHORITY`.
*
* - 0.7.0 (2021-03-17)
* - Fixed javadocs.
*
* - 0.8.0 (2021-03-18)
* - Fixed Intent extra types javadocs.
* - Added following to `TERMUX_SERVICE`:
* `EXTRA_PENDING_INTENT`, `EXTRA_RESULT_BUNDLE`,
* `EXTRA_STDOUT`, `EXTRA_STDERR`, `EXTRA_EXIT_CODE`,
* `EXTRA_ERR`, `EXTRA_ERRMSG`.
*
* - 0.9.0 (2021-03-18)
* - Fixed javadocs.
*
* - 0.10.0 (2021-03-19)
* - Added following to `TERMUX_SERVICE`:
* `EXTRA_SESSION_ACTION`,
* `VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY`,
* `VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY`,
* `VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY`
* `VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY`.
* - Added following to `RUN_COMMAND_SERVICE`:
* `EXTRA_SESSION_ACTION`.
*
* - 0.11.0 (2021-03-24)
* - Added following to `TERMUX_SERVICE`:
* `EXTRA_COMMAND_LABEL`, `EXTRA_COMMAND_DESCRIPTION`, `EXTRA_COMMAND_HELP`, `EXTRA_PLUGIN_API_HELP`.
* - Added following to `RUN_COMMAND_SERVICE`:
* `EXTRA_COMMAND_LABEL`, `EXTRA_COMMAND_DESCRIPTION`, `EXTRA_COMMAND_HELP`.
* - Updated `RESULT_BUNDLE` related extras with `PLUGIN_RESULT_BUNDLE` prefixes.
*
* - 0.12.0 (2021-03-25)
* - Added following to `TERMUX_SERVICE`:
* `EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH`,
* `EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH`.
*
* - 0.13.0 (2021-03-25)
* - Added following to `RUN_COMMAND_SERVICE`:
* `EXTRA_PENDING_INTENT`.
*
* - 0.14.0 (2021-03-25)
* - Added `FDROID_PACKAGES_BASE_URL`,
* `TERMUX_GITHUB_ORGANIZATION_NAME`, `TERMUX_GITHUB_ORGANIZATION_URL`,
* `TERMUX_GITHUB_REPO_NAME`, `TERMUX_GITHUB_REPO_URL`, `TERMUX_FDROID_PACKAGE_URL`,
* `TERMUX_API_GITHUB_REPO_NAME`,`TERMUX_API_GITHUB_REPO_URL`, `TERMUX_API_FDROID_PACKAGE_URL`,
* `TERMUX_BOOT_GITHUB_REPO_NAME`, `TERMUX_BOOT_GITHUB_REPO_URL`, `TERMUX_BOOT_FDROID_PACKAGE_URL`,
* `TERMUX_FLOAT_GITHUB_REPO_NAME`, `TERMUX_FLOAT_GITHUB_REPO_URL`, `TERMUX_FLOAT_FDROID_PACKAGE_URL`,
* `TERMUX_STYLING_GITHUB_REPO_NAME`, `TERMUX_STYLING_GITHUB_REPO_URL`, `TERMUX_STYLING_FDROID_PACKAGE_URL`,
* `TERMUX_TASKER_GITHUB_REPO_NAME`, `TERMUX_TASKER_GITHUB_REPO_URL`, `TERMUX_TASKER_FDROID_PACKAGE_URL`,
* `TERMUX_WIDGET_GITHUB_REPO_NAME`, `TERMUX_WIDGET_GITHUB_REPO_URL` `TERMUX_WIDGET_FDROID_PACKAGE_URL`.
*
* - 0.15.0 (2021-04-06)
* - Fixed some variables that had `PREFIX_` substring missing in their name.
* - Added `TERMUX_CRASH_LOG_FILE_PATH`, `TERMUX_CRASH_LOG_BACKUP_FILE_PATH`,
* `TERMUX_GITHUB_ISSUES_REPO_URL`, `TERMUX_API_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_BOOT_GITHUB_ISSUES_REPO_URL`, `TERMUX_FLOAT_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_STYLING_GITHUB_ISSUES_REPO_URL`, `TERMUX_TASKER_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_WIDGET_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_GITHUB_WIKI_REPO_URL`, `TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL`,
* `TERMUX_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_PACKAGES_GITHUB_REPO_URL`, `TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_GAME_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_GAME_PACKAGES_GITHUB_REPO_URL`, `TERMUX_GAME_PACKAGES_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_URL`, `TERMUX_SCIENCE_PACKAGES_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_ROOT_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_ROOT_PACKAGES_GITHUB_REPO_URL`, `TERMUX_ROOT_PACKAGES_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_URL`, `TERMUX_UNSTABLE_PACKAGES_GITHUB_ISSUES_REPO_URL`,
* `TERMUX_X11_PACKAGES_GITHUB_REPO_NAME`, `TERMUX_X11_PACKAGES_GITHUB_REPO_URL`, `TERMUX_X11_PACKAGES_GITHUB_ISSUES_REPO_URL`.
* - Added following to `RUN_COMMAND_SERVICE`:
* `RUN_COMMAND_API_HELP_URL`.
*
* - 0.16.0 (2021-04-06)
* - Added `TERMUX_SUPPORT_EMAIL`, `TERMUX_SUPPORT_EMAIL_URL`, `TERMUX_SUPPORT_EMAIL_MAILTO_URL`,
* `TERMUX_REDDIT_SUBREDDIT`, `TERMUX_REDDIT_SUBREDDIT_URL`.
* - The `TERMUX_SUPPORT_EMAIL_URL` value must be fixed later when email has been set up.
*
* - 0.17.0 (2021-04-07)
* - Added `TERMUX_APP_NOTIFICATION_CHANNEL_ID`, `TERMUX_APP_NOTIFICATION_CHANNEL_NAME`, `TERMUX_APP_NOTIFICATION_ID`,
* `TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID`, `TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME`, `TERMUX_RUN_COMMAND_NOTIFICATION_ID`,
* `TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID`, `TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME`,
* `TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID`, `TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME`.
* - Updated javadocs.
*/
/**
* A class that defines shared constants of the Termux app and its plugins.
* This class will be hosted by termux-shared lib and should be imported by other termux plugin
* apps as is instead of copying constants to random classes. The 3rd party apps can also import
* it for interacting with termux apps. If changes are made to this file, increment the version number
* and add an entry in the Changelog section above.
*
* Termux app default package name is "com.termux" and is used in {@link #TERMUX_PREFIX_DIR_PATH}.
* The binaries compiled for termux have {@link #TERMUX_PREFIX_DIR_PATH} hardcoded in them but it
* can be changed during compilation.
*
* The {@link #TERMUX_PACKAGE_NAME} must be the same as the applicationId of termux-app build.gradle
* since its also used by {@link #TERMUX_FILES_DIR_PATH}.
* If {@link #TERMUX_PACKAGE_NAME} is changed, then binaries, specially used in bootstrap need to be
* compiled appropriately. Check https://github.com/termux/termux-packages/wiki/Building-packages
* for more info.
*
* Ideally the only places where changes should be required if changing package name are the following:
* - The {@link #TERMUX_PACKAGE_NAME} in {@link TermuxConstants}.
* - The "applicationId" in "build.gradle" of termux-app. This is package name that android and app
* stores will use and is also the final package name stored in "AndroidManifest.xml".
* - The "manifestPlaceholders" values for {@link #TERMUX_PACKAGE_NAME} and *_APP_NAME in
* "build.gradle" of termux-app.
* - The "ENTITY" values for {@link #TERMUX_PACKAGE_NAME} and *_APP_NAME in "strings.xml" of
* termux-app and of termux-shared.
* - The "shortcut.xml" and "*_preferences.xml" files of termux-app since dynamic variables don't
* work in it.
* - Optionally the "package" in "AndroidManifest.xml" if modifying project structure of termux-app.
* This is package name for java classes project structure and is prefixed if activity and service
* names use dot (.) notation. This is currently not advisable since this will break lot of
* stuff, including termux-* packages.
* - Optionally the *_PATH variables in {@link TermuxConstants} containing the string "termux".
*
* Check https://developer.android.com/studio/build/application-id for info on "package" in
* "AndroidManifest.xml" and "applicationId" in "build.gradle".
*
* The {@link #TERMUX_PACKAGE_NAME} must be used in source code of Termux app and its plugins instead
* of hardcoded "com.termux" paths.
*/
public final class TermuxConstants {
/*
* Termux organization variables.
*/
/** Termux Github organization name */
public static final String TERMUX_GITHUB_ORGANIZATION_NAME = "termux"; // Default: "termux"
/** Termux Github organization url */
public static final String TERMUX_GITHUB_ORGANIZATION_URL = "https://github.com" + "/" + TERMUX_GITHUB_ORGANIZATION_NAME; // Default: "https://github.com/termux"
/** Termux support email */
public static final String TERMUX_SUPPORT_EMAIL = "support"; // Default: "support"
/** Termux support email url */
public static final String TERMUX_SUPPORT_EMAIL_URL = "email@example.com"; // Default: "email@example.com"
/** Termux support email mailto url */
public static final String TERMUX_SUPPORT_EMAIL_MAILTO_URL = "mailto:" + TERMUX_SUPPORT_EMAIL_URL; // Default: "mailto:email@example.com"
/** Termux Reddit subreddit */
public static final String TERMUX_REDDIT_SUBREDDIT = "r/termux"; // Default: "r/termux"
/** Termux Reddit subreddit url */
public static final String TERMUX_REDDIT_SUBREDDIT_URL = "https://www.reddit.com/r/termux"; // Default: "https://www.reddit.com/r/termux"
/** F-Droid packages base url */
public static final String FDROID_PACKAGES_BASE_URL = "https://f-droid.org/en/packages"; // Default: "https://f-droid.org/en/packages"
/*
* Termux and its plugin app and package names and urls.
*/
/** Termux app name */
public static final String TERMUX_APP_NAME = "Termux"; // Default: "Termux"
/** Termux package name */
public static final String TERMUX_PACKAGE_NAME = "com.termux"; // Default: "com.termux"
/** Termux Github repo name */
public static final String TERMUX_GITHUB_REPO_NAME = "termux-app"; // Default: "termux-app"
/** Termux Github repo url */
public static final String TERMUX_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-app"
/** Termux Github issues repo url */
public static final String TERMUX_GITHUB_ISSUES_REPO_URL = TERMUX_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-app/issues"
/** Termux Github wiki repo url */
public static final String TERMUX_GITHUB_WIKI_REPO_URL = TERMUX_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-app/wiki"
/** Termux F-Droid package url */
public static final String TERMUX_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux"
/** Termux API app name */
public static final String TERMUX_API_APP_NAME = "Termux:API"; // Default: "Termux:API"
/** Termux API app package name */
public static final String TERMUX_API_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".api"; // Default: "com.termux.api"
/** Termux API Github repo name */
public static final String TERMUX_API_GITHUB_REPO_NAME = "termux-api"; // Default: "termux-api"
/** Termux API Github repo url */
public static final String TERMUX_API_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_API_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-api"
/** Termux API Github issues repo url */
public static final String TERMUX_API_GITHUB_ISSUES_REPO_URL = TERMUX_API_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-api/issues"
/** Termux API F-Droid package url */
public static final String TERMUX_API_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_API_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.api"
/** Termux Boot app name */
public static final String TERMUX_BOOT_APP_NAME = "Termux:Boot"; // Default: "Termux:Boot"
/** Termux Boot app package name */
public static final String TERMUX_BOOT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".boot"; // Default: "com.termux.boot"
/** Termux Boot Github repo name */
public static final String TERMUX_BOOT_GITHUB_REPO_NAME = "termux-boot"; // Default: "termux-boot"
/** Termux Boot Github repo url */
public static final String TERMUX_BOOT_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_BOOT_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-boot"
/** Termux Boot Github issues repo url */
public static final String TERMUX_BOOT_GITHUB_ISSUES_REPO_URL = TERMUX_BOOT_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-boot/issues"
/** Termux Boot F-Droid package url */
public static final String TERMUX_BOOT_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_BOOT_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.boot"
/** Termux Float app name */
public static final String TERMUX_FLOAT_APP_NAME = "Termux:Float"; // Default: "Termux:Float"
/** Termux Float app package name */
public static final String TERMUX_FLOAT_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".window"; // Default: "com.termux.window"
/** Termux Float Github repo name */
public static final String TERMUX_FLOAT_GITHUB_REPO_NAME = "termux-float"; // Default: "termux-float"
/** Termux Float Github repo url */
public static final String TERMUX_FLOAT_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_FLOAT_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-float"
/** Termux Float Github issues repo url */
public static final String TERMUX_FLOAT_GITHUB_ISSUES_REPO_URL = TERMUX_FLOAT_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-float/issues"
/** Termux Float F-Droid package url */
public static final String TERMUX_FLOAT_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_FLOAT_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.window"
/** Termux Styling app name */
public static final String TERMUX_STYLING_APP_NAME = "Termux:Styling"; // Default: "Termux:Styling"
/** Termux Styling app package name */
public static final String TERMUX_STYLING_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".styling"; // Default: "com.termux.styling"
/** Termux Styling Github repo name */
public static final String TERMUX_STYLING_GITHUB_REPO_NAME = "termux-styling"; // Default: "termux-styling"
/** Termux Styling Github repo url */
public static final String TERMUX_STYLING_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_STYLING_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-styling"
/** Termux Styling Github issues repo url */
public static final String TERMUX_STYLING_GITHUB_ISSUES_REPO_URL = TERMUX_STYLING_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-styling/issues"
/** Termux Styling F-Droid package url */
public static final String TERMUX_STYLING_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_STYLING_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.styling"
/** Termux Tasker app name */
public static final String TERMUX_TASKER_APP_NAME = "Termux:Tasker"; // Default: "Termux:Tasker"
/** Termux Tasker app package name */
public static final String TERMUX_TASKER_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".tasker"; // Default: "com.termux.tasker"
/** Termux Tasker Github repo name */
public static final String TERMUX_TASKER_GITHUB_REPO_NAME = "termux-tasker"; // Default: "termux-tasker"
/** Termux Tasker Github repo url */
public static final String TERMUX_TASKER_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_TASKER_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-tasker"
/** Termux Tasker Github issues repo url */
public static final String TERMUX_TASKER_GITHUB_ISSUES_REPO_URL = TERMUX_TASKER_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-tasker/issues"
/** Termux Tasker F-Droid package url */
public static final String TERMUX_TASKER_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_TASKER_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.tasker"
/** Termux Widget app name */
public static final String TERMUX_WIDGET_APP_NAME = "Termux:Widget"; // Default: "Termux:Widget"
/** Termux Widget app package name */
public static final String TERMUX_WIDGET_PACKAGE_NAME = TERMUX_PACKAGE_NAME + ".widget"; // Default: "com.termux.widget"
/** Termux Widget Github repo name */
public static final String TERMUX_WIDGET_GITHUB_REPO_NAME = "termux-widget"; // Default: "termux-widget"
/** Termux Widget Github repo url */
public static final String TERMUX_WIDGET_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_WIDGET_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-widget"
/** Termux Widget Github issues repo url */
public static final String TERMUX_WIDGET_GITHUB_ISSUES_REPO_URL = TERMUX_WIDGET_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-widget/issues"
/** Termux Widget F-Droid package url */
public static final String TERMUX_WIDGET_FDROID_PACKAGE_URL = FDROID_PACKAGES_BASE_URL + "/" + TERMUX_WIDGET_PACKAGE_NAME; // Default: "https://f-droid.org/en/packages/com.termux.widget"
/*
* Termux packages urls.
*/
/** Termux Packages Github repo name */
public static final String TERMUX_PACKAGES_GITHUB_REPO_NAME = "termux-packages"; // Default: "termux-packages"
/** Termux Packages Github repo url */
public static final String TERMUX_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-packages"
/** Termux Packages Github issues repo url */
public static final String TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-packages/issues"
/** Termux Packages wiki repo url */
public static final String TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL = TERMUX_PACKAGES_GITHUB_REPO_URL + "/wiki"; // Default: "https://github.com/termux/termux-packages/wiki"
/** Termux Game Packages Github repo name */
public static final String TERMUX_GAME_PACKAGES_GITHUB_REPO_NAME = "game-packages"; // Default: "game-packages"
/** Termux Game Packages Github repo url */
public static final String TERMUX_GAME_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_GAME_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/game-packages"
/** Termux Game Packages Github issues repo url */
public static final String TERMUX_GAME_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_GAME_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/game-packages/issues"
/** Termux Science Packages Github repo name */
public static final String TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_NAME = "science-packages"; // Default: "science-packages"
/** Termux Science Packages Github repo url */
public static final String TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/science-packages"
/** Termux Science Packages Github issues repo url */
public static final String TERMUX_SCIENCE_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/science-packages/issues"
/** Termux Root Packages Github repo name */
public static final String TERMUX_ROOT_PACKAGES_GITHUB_REPO_NAME = "termux-root-packages"; // Default: "termux-root-packages"
/** Termux Root Packages Github repo url */
public static final String TERMUX_ROOT_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_ROOT_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/termux-root-packages"
/** Termux Root Packages Github issues repo url */
public static final String TERMUX_ROOT_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_ROOT_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/termux-root-packages/issues"
/** Termux Unstable Packages Github repo name */
public static final String TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_NAME = "unstable-packages"; // Default: "unstable-packages"
/** Termux Unstable Packages Github repo url */
public static final String TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/unstable-packages"
/** Termux Unstable Packages Github issues repo url */
public static final String TERMUX_UNSTABLE_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/unstable-packages/issues"
/** Termux X11 Packages Github repo name */
public static final String TERMUX_X11_PACKAGES_GITHUB_REPO_NAME = "x11-packages"; // Default: "x11-packages"
/** Termux X11 Packages Github repo url */
public static final String TERMUX_X11_PACKAGES_GITHUB_REPO_URL = TERMUX_GITHUB_ORGANIZATION_URL + "/" + TERMUX_X11_PACKAGES_GITHUB_REPO_NAME; // Default: "https://github.com/termux/x11-packages"
/** Termux X11 Packages Github issues repo url */
public static final String TERMUX_X11_PACKAGES_GITHUB_ISSUES_REPO_URL = TERMUX_X11_PACKAGES_GITHUB_REPO_URL + "/issues"; // Default: "https://github.com/termux/x11-packages/issues"
/*
* Termux app core directory paths.
*/
/** Termux app internal private app data directory path */
@SuppressLint("SdCardPath")
public static final String INTERNAL_PRIVATE_APP_DATA_DIR_PATH = "/data/data/" + TERMUX_PACKAGE_NAME; // Default: "/data/data/com.termux"
/** Termux app internal private app data directory */
public static final File INTERNAL_PRIVATE_APP_DATA_DIR = new File(INTERNAL_PRIVATE_APP_DATA_DIR_PATH);
/** Termux app Files directory path */
public static final String TERMUX_FILES_DIR_PATH = INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "/files"; // Default: "/data/data/com.termux/files"
/** Termux app Files directory */
public static final File TERMUX_FILES_DIR = new File(TERMUX_FILES_DIR_PATH);
/** Termux app $PREFIX directory path */
public static final String TERMUX_PREFIX_DIR_PATH = TERMUX_FILES_DIR_PATH + "/usr"; // Default: "/data/data/com.termux/files/usr"
/** Termux app $PREFIX directory */
public static final File TERMUX_PREFIX_DIR = new File(TERMUX_PREFIX_DIR_PATH);
/** Termux app $PREFIX/bin directory path */
public static final String TERMUX_BIN_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/bin"; // Default: "/data/data/com.termux/files/usr/bin"
/** Termux app $PREFIX/bin directory */
public static final File TERMUX_BIN_PREFIX_DIR = new File(TERMUX_BIN_PREFIX_DIR_PATH);
/** Termux app $PREFIX/etc directory path */
public static final String TERMUX_ETC_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/etc"; // Default: "/data/data/com.termux/files/usr/etc"
/** Termux app $PREFIX/etc directory */
public static final File TERMUX_ETC_PREFIX_DIR = new File(TERMUX_ETC_PREFIX_DIR_PATH);
/** Termux app $PREFIX/include directory path */
public static final String TERMUX_INCLUDE_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/include"; // Default: "/data/data/com.termux/files/usr/include"
/** Termux app $PREFIX/include directory */
public static final File TERMUX_INCLUDE_PREFIX_DIR = new File(TERMUX_INCLUDE_PREFIX_DIR_PATH);
/** Termux app $PREFIX/lib directory path */
public static final String TERMUX_LIB_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/lib"; // Default: "/data/data/com.termux/files/usr/lib"
/** Termux app $PREFIX/lib directory */
public static final File TERMUX_LIB_PREFIX_DIR = new File(TERMUX_LIB_PREFIX_DIR_PATH);
/** Termux app $PREFIX/libexec directory path */
public static final String TERMUX_LIBEXEC_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/libexec"; // Default: "/data/data/com.termux/files/usr/libexec"
/** Termux app $PREFIX/libexec directory */
public static final File TERMUX_LIBEXEC_PREFIX_DIR = new File(TERMUX_LIBEXEC_PREFIX_DIR_PATH);
/** Termux app $PREFIX/share directory path */
public static final String TERMUX_SHARE_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/share"; // Default: "/data/data/com.termux/files/usr/share"
/** Termux app $PREFIX/share directory */
public static final File TERMUX_SHARE_PREFIX_DIR = new File(TERMUX_SHARE_PREFIX_DIR_PATH);
/** Termux app $PREFIX/tmp and $TMPDIR directory path */
public static final String TERMUX_TMP_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/tmp"; // Default: "/data/data/com.termux/files/usr/tmp"
/** Termux app $PREFIX/tmp and $TMPDIR directory */
public static final File TERMUX_TMP_PREFIX_DIR = new File(TERMUX_TMP_PREFIX_DIR_PATH);
/** Termux app $PREFIX/var directory path */
public static final String TERMUX_VAR_PREFIX_DIR_PATH = TERMUX_PREFIX_DIR_PATH + "/var"; // Default: "/data/data/com.termux/files/usr/var"
/** Termux app $PREFIX/var directory */
public static final File TERMUX_VAR_PREFIX_DIR = new File(TERMUX_VAR_PREFIX_DIR_PATH);
/** Termux app usr-staging directory path */
public static final String TERMUX_STAGING_PREFIX_DIR_PATH = TERMUX_FILES_DIR_PATH + "/usr-staging"; // Default: "/data/data/com.termux/files/usr-staging"
/** Termux app usr-staging directory */
public static final File TERMUX_STAGING_PREFIX_DIR = new File(TERMUX_STAGING_PREFIX_DIR_PATH);
/** Termux app $HOME directory path */
public static final String TERMUX_HOME_DIR_PATH = TERMUX_FILES_DIR_PATH + "/home"; // Default: "/data/data/com.termux/files/home"
/** Termux app $HOME directory */
public static final File TERMUX_HOME_DIR = new File(TERMUX_HOME_DIR_PATH);
/** Termux app config home directory path */
public static final String TERMUX_CONFIG_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/.config/termux"; // Default: "/data/data/com.termux/files/home/.config/termux"
/** Termux app config home directory */
public static final File TERMUX_CONFIG_HOME_DIR = new File(TERMUX_CONFIG_HOME_DIR_PATH);
/** Termux app data home directory path */
public static final String TERMUX_DATA_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/.termux"; // Default: "/data/data/com.termux/files/home/.termux"
/** Termux app data home directory */
public static final File TERMUX_DATA_HOME_DIR = new File(TERMUX_DATA_HOME_DIR_PATH);
/** Termux app storage home directory path */
public static final String TERMUX_STORAGE_HOME_DIR_PATH = TERMUX_HOME_DIR_PATH + "/storage"; // Default: "/data/data/com.termux/files/home/storage"
/** Termux app storage home directory */
public static final File TERMUX_STORAGE_HOME_DIR = new File(TERMUX_STORAGE_HOME_DIR_PATH);
/*
* Termux app and plugin preferences and properties file paths.
*/
/** Termux app default SharedPreferences file basename without extension */
public static final String TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_PACKAGE_NAME + "_preferences"; // Default: "com.termux_preferences"
/** Termux API app default SharedPreferences file basename without extension */
public static final String TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_API_PACKAGE_NAME + "_preferences"; // Default: "com.termux.api_preferences"
/** Termux Boot app default SharedPreferences file basename without extension */
public static final String TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_BOOT_PACKAGE_NAME + "_preferences"; // Default: "com.termux.boot_preferences"
/** Termux Float app default SharedPreferences file basename without extension */
public static final String TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_FLOAT_PACKAGE_NAME + "_preferences"; // Default: "com.termux.window_preferences"
/** Termux Styling app default SharedPreferences file basename without extension */
public static final String TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_STYLING_PACKAGE_NAME + "_preferences"; // Default: "com.termux.styling_preferences"
/** Termux Tasker app default SharedPreferences file basename without extension */
public static final String TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_TASKER_PACKAGE_NAME + "_preferences"; // Default: "com.termux.tasker_preferences"
/** Termux Widget app default SharedPreferences file basename without extension */
public static final String TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION = TERMUX_WIDGET_PACKAGE_NAME + "_preferences"; // Default: "com.termux.widget_preferences"
/** Termux app termux.properties primary file path */
public static final String TERMUX_PROPERTIES_PRIMARY_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.termux/termux.properties"
/** Termux app termux.properties primary file */
public static final File TERMUX_PROPERTIES_PRIMARY_FILE = new File(TERMUX_PROPERTIES_PRIMARY_FILE_PATH);
/** Termux app termux.properties secondary file path */
public static final String TERMUX_PROPERTIES_SECONDARY_FILE_PATH = TERMUX_CONFIG_HOME_DIR_PATH + "/termux.properties"; // Default: "/data/data/com.termux/files/home/.config/termux/termux.properties"
/** Termux app termux.properties secondary file */
public static final File TERMUX_PROPERTIES_SECONDARY_FILE = new File(TERMUX_PROPERTIES_SECONDARY_FILE_PATH);
/** Termux app and Termux:Styling colors.properties file path */
public static final String TERMUX_COLOR_PROPERTIES_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/colors.properties"; // Default: "/data/data/com.termux/files/home/.termux/colors.properties"
/** Termux app and Termux:Styling colors.properties file */
public static final File TERMUX_COLOR_PROPERTIES_FILE = new File(TERMUX_COLOR_PROPERTIES_FILE_PATH);
/** Termux app and Termux:Styling font.ttf file path */
public static final String TERMUX_FONT_FILE_PATH = TERMUX_DATA_HOME_DIR_PATH + "/font.ttf"; // Default: "/data/data/com.termux/files/home/.termux/font.ttf"
/** Termux app and Termux:Styling font.ttf file */
public static final File TERMUX_FONT_FILE = new File(TERMUX_FONT_FILE_PATH);
/** Termux app and plugins crash log file path */
public static final String TERMUX_CRASH_LOG_FILE_PATH = TERMUX_HOME_DIR_PATH + "/crash_log.md"; // Default: "/data/data/com.termux/files/home/crash_log.md"
/** Termux app and plugins crash log backup file path */
public static final String TERMUX_CRASH_LOG_BACKUP_FILE_PATH = TERMUX_HOME_DIR_PATH + "/crash_log_backup.md"; // Default: "/data/data/com.termux/files/home/crash_log_backup.md"
/*
* Termux app plugin specific paths.
*/
/** Termux app directory path to store scripts to be run at boot by Termux:Boot */
public static final String TERMUX_BOOT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/boot"; // Default: "/data/data/com.termux/files/home/.termux/boot"
/** Termux app directory to store scripts to be run at boot by Termux:Boot */
public static final File TERMUX_BOOT_SCRIPTS_DIR = new File(TERMUX_BOOT_SCRIPTS_DIR_PATH);
/** Termux app directory path to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */
public static final String TERMUX_SHORTCUT_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts"
/** Termux app directory to store foreground scripts that can be run by the termux launcher widget provided by Termux:Widget */
public static final File TERMUX_SHORTCUT_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_SCRIPTS_DIR_PATH);
/** Termux app directory path to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */
public static final String TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/shortcuts/tasks"; // Default: "/data/data/com.termux/files/home/.termux/shortcuts/tasks"
/** Termux app directory to store background scripts that can be run by the termux launcher widget provided by Termux:Widget */
public static final File TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR = new File(TERMUX_SHORTCUT_TASKS_SCRIPTS_DIR_PATH);
/** Termux app directory path to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */
public static final String TERMUX_TASKER_SCRIPTS_DIR_PATH = TERMUX_DATA_HOME_DIR_PATH + "/tasker"; // Default: "/data/data/com.termux/files/home/.termux/tasker"
/** Termux app directory to store scripts to be run by 3rd party twofortyfouram locale plugin host apps like Tasker app via the Termux:Tasker plugin client */
public static final File TERMUX_TASKER_SCRIPTS_DIR = new File(TERMUX_TASKER_SCRIPTS_DIR_PATH);
/*
* Termux app and plugins notification variables.
*/
/** Termux app notification channel id used by {@link TERMUX_APP.TERMUX_SERVICE} */
public static final String TERMUX_APP_NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
/** Termux app notification channel name used by {@link TERMUX_APP.TERMUX_SERVICE} */
public static final String TERMUX_APP_NOTIFICATION_CHANNEL_NAME = TermuxConstants.TERMUX_APP_NAME + " App";
/** Termux app unique notification id used by {@link TERMUX_APP.TERMUX_SERVICE} */
public static final int TERMUX_APP_NOTIFICATION_ID = 1337;
/** Termux app notification channel id used by {@link TERMUX_APP.RUN_COMMAND_SERVICE} */
public static final String TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel";
/** Termux app notification channel name used by {@link TERMUX_APP.RUN_COMMAND_SERVICE} */
public static final String TERMUX_RUN_COMMAND_NOTIFICATION_CHANNEL_NAME = TermuxConstants.TERMUX_APP_NAME + " RunCommandService";
/** Termux app unique notification id used by {@link TERMUX_APP.RUN_COMMAND_SERVICE} */
public static final int TERMUX_RUN_COMMAND_NOTIFICATION_ID = 1338;
/** Termux app notification channel id used for plugin command errors */
public static final String TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_ID = "termux_plugin_command_errors_notification_channel";
/** Termux app notification channel name used for plugin command errors */
public static final String TERMUX_PLUGIN_COMMAND_ERRORS_NOTIFICATION_CHANNEL_NAME = TermuxConstants.TERMUX_APP_NAME + " Plugin Commands Errors";
/** Termux app notification channel id used for crash reports */
public static final String TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_ID = "termux_crash_reports_notification_channel";
/** Termux app notification channel name used for crash reports */
public static final String TERMUX_CRASH_REPORTS_NOTIFICATION_CHANNEL_NAME = TermuxConstants.TERMUX_APP_NAME + " Crash Reports";
/*
* Termux app and plugins miscellaneous variables.
*/
/** Android OS permission declared by Termux app in AndroidManifest.xml which can be requested by 3rd party apps to run various commands in Termux app context */
public static final String PERMISSION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".permission.RUN_COMMAND"; // Default: "com.termux.permission.RUN_COMMAND"
/** Termux property defined in termux.properties file as a secondary check to PERMISSION_RUN_COMMAND to allow 3rd party apps to run various commands in Termux app context */
public static final String PROP_ALLOW_EXTERNAL_APPS = "allow-external-apps"; // Default: "allow-external-apps"
/** Default value for {@link #PROP_ALLOW_EXTERNAL_APPS} */
public static final String PROP_DEFAULT_VALUE_ALLOW_EXTERNAL_APPS = "false"; // Default: "false"
/** The broadcast action sent when Termux App opens */
public static final String BROADCAST_TERMUX_OPENED = TERMUX_PACKAGE_NAME + ".app.OPENED";
/** The Uri authority for Termux app file shares */
public static final String TERMUX_FILE_SHARE_URI_AUTHORITY = TERMUX_PACKAGE_NAME + ".files"; // Default: "com.termux.files"
/**
* Termux app constants.
*/
public static final class TERMUX_APP {
/** Termux app core activity name. */
public static final String TERMUX_ACTIVITY_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxActivity"; // Default: "com.termux.app.TermuxActivity"
/**
* Termux app core activity.
*/
public static final class TERMUX_ACTIVITY {
/** Intent action to start termux failsafe session */
public static final String ACTION_FAILSAFE_SESSION = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.failsafe_session"; // Default: "com.termux.app.failsafe_session"
/** Intent action to make termux reload its termux session styling */
public static final String ACTION_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style"
/** Intent {@code String} extra for what to reload for the TERMUX_ACTIVITY.ACTION_RELOAD_STYLE intent */
public static final String EXTRA_RELOAD_STYLE = TermuxConstants.TERMUX_PACKAGE_NAME + ".app.reload_style"; // Default: "com.termux.app.reload_style"
}
/** Termux app core service name. */
public static final String TERMUX_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.TermuxService"; // Default: "com.termux.app.TermuxService"
/**
* Termux app core service.
*/
public static final class TERMUX_SERVICE {
/** Intent action to stop TERMUX_SERVICE */
public static final String ACTION_STOP_SERVICE = TERMUX_PACKAGE_NAME + ".service_stop"; // Default: "com.termux.service_stop"
/** Intent action to make TERMUX_SERVICE acquire a wakelock */
public static final String ACTION_WAKE_LOCK = TERMUX_PACKAGE_NAME + ".service_wake_lock"; // Default: "com.termux.service_wake_lock"
/** Intent action to make TERMUX_SERVICE release wakelock */
public static final String ACTION_WAKE_UNLOCK = TERMUX_PACKAGE_NAME + ".service_wake_unlock"; // Default: "com.termux.service_wake_unlock"
/** Intent action to execute command with TERMUX_SERVICE */
public static final String ACTION_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".service_execute"; // Default: "com.termux.service_execute"
/** Uri scheme for paths sent via intent to TERMUX_SERVICE */
public static final String URI_SCHEME_SERVICE_EXECUTE = TERMUX_PACKAGE_NAME + ".file"; // Default: "com.termux.file"
/** Intent {@code String[]} extra for command arguments for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".execute.arguments"; // Default: "com.termux.execute.arguments"
/** Intent {@code String} extra for command current working directory for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".execute.cwd"; // Default: "com.termux.execute.cwd"
/** Intent {@code boolean} extra for command background mode for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".execute.background"; // Default: "com.termux.execute.background"
/** Intent {@code String} extra for session action for foreground commands for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_SESSION_ACTION = TERMUX_PACKAGE_NAME + ".execute.session_action"; // Default: "com.termux.execute.session_action"
/** Intent {@code String} extra for label of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_COMMAND_LABEL = TERMUX_PACKAGE_NAME + ".execute.command_label"; // Default: "com.termux.execute.command_label"
/** Intent markdown {@code String} extra for description of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".execute.command_description"; // Default: "com.termux.execute.command_description"
/** Intent markdown {@code String} extra for help of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".execute.command_help"; // Default: "com.termux.execute.command_help"
/** Intent markdown {@code String} extra for help of the plugin API for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent (Internal Use Only) */
public static final String EXTRA_PLUGIN_API_HELP = TERMUX_PACKAGE_NAME + ".execute.plugin_api_help"; // Default: "com.termux.execute.plugin_help"
/** Intent {@code Parcelable} extra containing pending intent for the execute command caller */
public static final String EXTRA_PENDING_INTENT = "pendingIntent"; // Default: "pendingIntent"
/** The value for {@link #EXTRA_SESSION_ACTION} extra that will set the new session as
* the current session and will start {@link TERMUX_ACTIVITY} if its not running to bring
* the new session to foreground.
*/
public static final int VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY = 0;
/** The value for {@link #EXTRA_SESSION_ACTION} extra that will keep any existing session
* as the current session and will start {@link TERMUX_ACTIVITY} if its not running to
* bring the existing session to foreground. The new session will be added to the left
* sidebar in the sessions list.
*/
public static final int VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY = 1;
/** The value for {@link #EXTRA_SESSION_ACTION} extra that will set the new session as
* the current session but will not start {@link TERMUX_ACTIVITY} if its not running
* and session(s) will be seen in Termux notification and can be clicked to bring new
* session to foreground. If the {@link TERMUX_ACTIVITY} is already running, then this
* will behave like {@link #VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY}.
*/
public static final int VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY = 2;
/** The value for {@link #EXTRA_SESSION_ACTION} extra that will keep any existing session
* as the current session but will not start {@link TERMUX_ACTIVITY} if its not running
* and session(s) will be seen in Termux notification and can be clicked to bring
* existing session to foreground. If the {@link TERMUX_ACTIVITY} is already running,
* then this will behave like {@link #VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY}.
*/
public static final int VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY = 3;
/** Intent {@code Bundle} extra to store result of execute command that is sent back for the
* TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent if the {@link #EXTRA_PENDING_INTENT} is not
* {@code null} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE = "result"; // Default: "result"
/** Intent {@code String} extra for stdout value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT = "stdout"; // Default: "stdout"
/** Intent {@code String} extra for original length of stdout value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT_ORIGINAL_LENGTH = "stdout_original_length"; // Default: "stdout_original_length"
/** Intent {@code String} extra for stderr value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDERR = "stderr"; // Default: "stderr"
/** Intent {@code String} extra for original length of stderr value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDERR_ORIGINAL_LENGTH = "stderr_original_length"; // Default: "stderr_original_length"
/** Intent {@code int} extra for exit code value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE = "exitCode"; // Default: "exitCode"
/** Intent {@code int} extra for err value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_ERR = "err"; // Default: "err"
/** Intent {@code String} extra for errmsg value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */
public static final String EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG = "errmsg"; // Default: "errmsg"
}
/** Termux app run command service name. */
public static final String RUN_COMMAND_SERVICE_NAME = TERMUX_PACKAGE_NAME + ".app.RunCommandService"; // Termux app service to receive commands from 3rd party apps "com.termux.app.RunCommandService"
/**
* Termux app run command service to receive commands sent by 3rd party apps.
*/
public static final class RUN_COMMAND_SERVICE {
/** Termux RUN_COMMAND Intent help url */
public static final String RUN_COMMAND_API_HELP_URL = TERMUX_GITHUB_WIKI_REPO_URL + "/RUN_COMMAND-Intent"; // Default: "https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent"
/** Intent action to execute command with RUN_COMMAND_SERVICE */
public static final String ACTION_RUN_COMMAND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND"; // Default: "com.termux.RUN_COMMAND"
/** Intent {@code String} extra for absolute path of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_COMMAND_PATH = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PATH"; // Default: "com.termux.RUN_COMMAND_PATH"
/** Intent {@code String[]} extra for any arguments to pass to command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_ARGUMENTS = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_ARGUMENTS"; // Default: "com.termux.RUN_COMMAND_ARGUMENTS"
/** Intent {@code String} extra for current working directory of command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_WORKDIR = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_WORKDIR"; // Default: "com.termux.RUN_COMMAND_WORKDIR"
/** Intent {@code boolean} extra for whether to run command in background or foreground terminal session for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_BACKGROUND"; // Default: "com.termux.RUN_COMMAND_BACKGROUND"
/** Intent {@code String} extra for session action of foreground commands for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_SESSION_ACTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_SESSION_ACTION"; // Default: "com.termux.RUN_COMMAND_SESSION_ACTION"
/** Intent {@code String} extra for label of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_COMMAND_LABEL = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_LABEL"; // Default: "com.termux.RUN_COMMAND_COMMAND_LABEL"
/** Intent markdown {@code String} extra for description of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_DESCRIPTION"; // Default: "com.termux.RUN_COMMAND_COMMAND_DESCRIPTION"
/** Intent markdown {@code String} extra for help of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_HELP"; // Default: "com.termux.RUN_COMMAND_COMMAND_HELP"
/** Intent {@code Parcelable} extra containing pending intent for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */
public static final String EXTRA_PENDING_INTENT = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_PENDING_INTENT"; // Default: "com.termux.RUN_COMMAND_PENDING_INTENT"
}
}
/**
* Termux:Styling app constants.
*/
public static final class TERMUX_STYLING {
/** Termux:Styling app core activity name. */
public static final String TERMUX_STYLING_ACTIVITY_NAME = TERMUX_STYLING_PACKAGE_NAME + ".TermuxStyleActivity"; // Default: "com.termux.styling.TermuxStyleActivity"
}
}

View File

@@ -0,0 +1,365 @@
package com.termux.shared.termux;
import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Build;
import androidx.annotation.NonNull;
import com.google.common.base.Joiner;
import com.termux.shared.R;
import com.termux.shared.logger.Logger;
import com.termux.shared.markdown.MarkdownUtils;
import com.termux.shared.packages.PackageUtils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class TermuxUtils {
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_API_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxAPIPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_API_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_BOOT_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxBootPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_BOOT_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_FLOAT_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxFloatPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_STYLING_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxStylingPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_STYLING_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_TASKER_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxTaskerPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME);
}
/**
* Get the {@link Context} for {@link TermuxConstants#TERMUX_WIDGET_PACKAGE_NAME} package.
*
* @param context The {@link Context} to use to get the {@link Context} of the package.
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
*/
public static Context getTermuxWidgetPackageContext(@NonNull Context context) {
return PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_WIDGET_PACKAGE_NAME);
}
/**
* Send the {@link TermuxConstants#BROADCAST_TERMUX_OPENED} broadcast to notify apps that Termux
* app has been opened.
*
* @param context The Context to send the broadcast.
*/
public static void sendTermuxOpenedBroadcast(@NonNull Context context) {
if (context == null) return;
Intent broadcast = new Intent(TermuxConstants.BROADCAST_TERMUX_OPENED);
List<ResolveInfo> matches = context.getPackageManager().queryBroadcastReceivers(broadcast, 0);
// send broadcast to registered Termux receivers
// this technique is needed to work around broadcast changes that Oreo introduced
for (ResolveInfo info : matches) {
Intent explicitBroadcast = new Intent(broadcast);
ComponentName cname = new ComponentName(info.activityInfo.applicationInfo.packageName,
info.activityInfo.name);
explicitBroadcast.setComponent(cname);
context.sendBroadcast(explicitBroadcast);
}
}
/**
* Get a markdown {@link String} for the app info. If the {@code context} passed is different
* from the {@link TermuxConstants#TERMUX_PACKAGE_NAME} package context, then this function
* must have been called by a different package like a plugin, so we return info for both packages
* if {@code returnTermuxPackageInfoToo} is {@code true}.
*
* @param currentPackageContext The context of current package.
* @param returnTermuxPackageInfoToo If set to {@code true}, then will return info of the
* {@link TermuxConstants#TERMUX_PACKAGE_NAME} package as well if its different from current package.
* @return Returns the markdown {@link String}.
*/
public static String getAppInfoMarkdownString(@NonNull final Context currentPackageContext, final boolean returnTermuxPackageInfoToo) {
if (currentPackageContext == null) return "null";
StringBuilder markdownString = new StringBuilder();
Context termuxPackageContext = getTermuxPackageContext(currentPackageContext);
String termuxPackageName = null;
String termuxAppName = null;
if(termuxPackageContext != null) {
termuxPackageName = PackageUtils.getPackageNameForPackage(termuxPackageContext);
termuxAppName = PackageUtils.getAppNameForPackage(termuxPackageContext);
}
String currentPackageName = PackageUtils.getPackageNameForPackage(currentPackageContext);
String currentAppName = PackageUtils.getAppNameForPackage(currentPackageContext);
boolean isTermuxPackage = (termuxPackageName != null && termuxPackageName.equals(currentPackageName));
if(returnTermuxPackageInfoToo && !isTermuxPackage)
markdownString.append("## ").append(currentAppName).append(" App Info (Current)\n");
else
markdownString.append("## ").append(currentAppName).append(" App Info\n");
markdownString.append(getAppInfoMarkdownStringInner(currentPackageContext));
if(returnTermuxPackageInfoToo && !isTermuxPackage) {
markdownString.append("\n\n## ").append(termuxAppName).append(" App Info\n");
markdownString.append(getAppInfoMarkdownStringInner(termuxPackageContext));
}
markdownString.append("\n##\n");
return markdownString.toString();
}
/**
* Get a markdown {@link String} for the app info for the package associated with the {@code context}.
*
* @param context The context for operations for the package.
* @return Returns the markdown {@link String}.
*/
public static String getAppInfoMarkdownStringInner(@NonNull final Context context) {
StringBuilder markdownString = new StringBuilder();
appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
appendPropertyToMarkdown(markdownString,"VERSION_NAME", PackageUtils.getVersionNameForPackage(context));
appendPropertyToMarkdown(markdownString,"VERSION_CODE", PackageUtils.getVersionCodeForPackage(context));
appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
appendPropertyToMarkdown(markdownString,"IS_DEBUG_BUILD", PackageUtils.isAppForPackageADebugBuild(context));
return markdownString.toString();
}
/**
* Get a markdown {@link String} for the device info.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getDeviceInfoMarkdownString(@NonNull final Context context) {
if (context == null) return "null";
// Some properties cannot be read with {@link System#getProperty(String)} but can be read
// directly by running getprop command
Properties systemProperties = getSystemProperties();
StringBuilder markdownString = new StringBuilder();
markdownString.append("## Device Info");
markdownString.append("\n\n### Software\n");
appendPropertyToMarkdown(markdownString,"OS_VERSION", getSystemPropertyWithAndroidAPI("os.version"));
appendPropertyToMarkdown(markdownString, "SDK_INT", Build.VERSION.SDK_INT);
// If its a release version
if ("REL".equals(Build.VERSION.CODENAME))
appendPropertyToMarkdown(markdownString, "RELEASE", Build.VERSION.RELEASE);
else
appendPropertyToMarkdown(markdownString, "CODENAME", Build.VERSION.CODENAME);
appendPropertyToMarkdown(markdownString, "INCREMENTAL", Build.VERSION.INCREMENTAL);
appendPropertyToMarkdownIfSet(markdownString, "SECURITY_PATCH", systemProperties.getProperty("ro.build.version.security_patch"));
appendPropertyToMarkdownIfSet(markdownString, "IS_DEBUGGABLE", systemProperties.getProperty("ro.debuggable"));
appendPropertyToMarkdownIfSet(markdownString, "IS_EMULATOR", systemProperties.getProperty("ro.boot.qemu"));
appendPropertyToMarkdownIfSet(markdownString, "IS_TREBLE_ENABLED", systemProperties.getProperty("ro.treble.enabled"));
appendPropertyToMarkdown(markdownString, "TYPE", Build.TYPE);
appendPropertyToMarkdown(markdownString, "TAGS", Build.TAGS);
markdownString.append("\n\n### Hardware\n");
appendPropertyToMarkdown(markdownString, "MANUFACTURER", Build.MANUFACTURER);
appendPropertyToMarkdown(markdownString, "BRAND", Build.BRAND);
appendPropertyToMarkdown(markdownString, "MODEL", Build.MODEL);
appendPropertyToMarkdown(markdownString, "PRODUCT", Build.PRODUCT);
appendPropertyToMarkdown(markdownString, "DISPLAY", Build.DISPLAY);
appendPropertyToMarkdown(markdownString, "ID", Build.ID);
appendPropertyToMarkdown(markdownString, "BOARD", Build.BOARD);
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
markdownString.append("\n##\n");
return markdownString.toString();
}
/**
* Get a markdown {@link String} for reporting an issue.
*
* @param context The context for operations.
* @return Returns the markdown {@link String}.
*/
public static String getReportIssueMarkdownString(@NonNull final Context context) {
if (context == null) return "null";
StringBuilder markdownString = new StringBuilder();
markdownString.append("## Where To Report An Issue");
markdownString.append("\n\n").append(context.getString(R.string.msg_report_issue)).append("\n");
//markdownString.append("\n\n### Email\n");
//markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_SUPPORT_EMAIL, TermuxConstants.TERMUX_SUPPORT_EMAIL_MAILTO_URL)).append(" ");
markdownString.append("\n\n### Reddit\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_REDDIT_SUBREDDIT, TermuxConstants.TERMUX_REDDIT_SUBREDDIT_URL)).append(" ");
markdownString.append("\n\n### Github Issues for Termux apps\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_BOOT_APP_NAME, TermuxConstants.TERMUX_BOOT_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_FLOAT_APP_NAME, TermuxConstants.TERMUX_FLOAT_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_STYLING_APP_NAME, TermuxConstants.TERMUX_STYLING_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_TASKER_APP_NAME, TermuxConstants.TERMUX_TASKER_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIDGET_APP_NAME, TermuxConstants.TERMUX_WIDGET_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n\n### Github Issues for Termux packages\n");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_GAME_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_GAME_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_SCIENCE_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_SCIENCE_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_ROOT_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_ROOT_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_UNSTABLE_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_UNSTABLE_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_X11_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_X11_PACKAGES_GITHUB_ISSUES_REPO_URL)).append(" ");
markdownString.append("\n##\n");
return markdownString.toString();
}
public static Properties getSystemProperties() {
Properties systemProperties = new Properties();
// getprop commands returns values in the format `[key]: [value]`
// Regex matches string starting with a literal `[`,
// followed by one or more characters that do not match a closing square bracket as the key,
// followed by a literal `]: [`,
// followed by one or more characters as the value,
// followed by string ending with literal `]`
// multiline values will be ignored
Pattern propertiesPattern = Pattern.compile("^\\[([^]]+)]: \\[(.+)]$");
try {
Process process = new ProcessBuilder()
.command("/system/bin/getprop")
.redirectErrorStream(true)
.start();
InputStream inputStream = process.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line, key, value;
while ((line = bufferedReader.readLine()) != null) {
Matcher matcher = propertiesPattern.matcher(line);
if (matcher.matches()) {
key = matcher.group(1);
value = matcher.group(2);
if(key != null && value != null && !key.isEmpty() && !value.isEmpty())
systemProperties.put(key, value);
}
}
bufferedReader.close();
process.destroy();
} catch (IOException e) {
Logger.logStackTraceWithMessage("Failed to get run \"/system/bin/getprop\" to get system properties.", e);
}
//for (String key : systemProperties.stringPropertyNames()) {
// Logger.logVerbose(key + ": " + systemProperties.get(key));
//}
return systemProperties;
}
private static String getSystemPropertyWithAndroidAPI(@NonNull String property) {
try {
return System.getProperty(property);
} catch (Exception e) {
Logger.logVerbose("Failed to get system property \"" + property + "\":" + e.getMessage());
return null;
}
}
private static void appendPropertyToMarkdownIfSet(StringBuilder markdownString, String label, Object value) {
if(value == null) return;
if(value instanceof String && (((String) value).isEmpty()) || "REL".equals(value)) return;
markdownString.append("\n").append(getPropertyMarkdown(label, value));
}
private static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
markdownString.append("\n").append(getPropertyMarkdown(label, value));
}
private static String getPropertyMarkdown(String label, Object value) {
return MarkdownUtils.getSingleLineMarkdownStringEntry(label, value, "-");
}
public static String getCurrentTimeStamp() {
@SuppressLint("SimpleDateFormat")
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss z");
df.setTimeZone(TimeZone.getTimeZone("UTC"));
return df.format(new Date());
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_markdown_code_inline">#1F000000</color>
<color name="background_markdown_code_block">#0F000000</color>
</resources>

View File

@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE resources [
<!ENTITY TERMUX_PACKAGE_NAME "com.termux">
<!ENTITY TERMUX_APP_NAME "Termux">
<!ENTITY TERMUX_API_APP_NAME "Termux:API">
<!ENTITY TERMUX_BOOT_APP_NAME "Termux:Boot">
<!ENTITY TERMUX_FLOAT_APP_NAME "Termux:Float">
<!ENTITY TERMUX_STYLING_APP_NAME "Termux:Styling">
<!ENTITY TERMUX_TASKER_APP_NAME "Termux:Tasker">
<!ENTITY TERMUX_WIDGET_APP_NAME "Termux:Widget">
]>
<resources>
<!-- FileUtils -->
<string name="error_executable_required">Executable required.</string>
<string name="error_null_or_empty_parameter">The %1$s is to \"%2$s\" null or empty.</string>
<string name="error_null_or_empty_regular_file_path">The regular file path is null or empty.</string>
<string name="error_null_or_empty_regular_file">The regular file is null or empty.</string>
<string name="error_null_or_empty_executable_file_path">The executable file path is null or empty.</string>
<string name="error_null_or_empty_executable_file">The executable file is null or empty.</string>
<string name="error_null_or_empty_directory_file_path">The directory file path is null or empty.</string>
<string name="error_null_or_empty_directory_file">The directory file is null or empty.</string>
<string name="error_file_not_found_at_path">The %1$s is not found at path \"%2$s\".</string>
<string name="error_no_regular_file_found">Regular file not found at %1$s path.</string>
<string name="error_not_a_regular_file">The %1$s at path \"%2$s\" is not a regular file.</string>
<string name="error_non_regular_file_found">Non-regular file found at %1$s path.</string>
<string name="error_non_directory_file_found">Non-directory file found at %1$s path.</string>
<string name="error_non_symlink_file_found">Non-symlink file found at %1$s path.</string>
<string name="error_file_not_an_allowed_file_type">The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".</string>
<string name="error_validate_file_existence_and_permissions_failed_with_exception">Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_validate_directory_existence_and_permissions_failed_with_exception">Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_creating_file_failed">Creating %1$s at path \"%2$s\" failed.</string>
<string name="error_creating_file_failed_with_exception">Creating %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_cannot_overwrite_a_non_symlink_file_type">Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.</string>
<string name="error_creating_symlink_file_failed_with_exception">Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
<string name="error_copying_or_moving_file_failed_with_exception">%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s</string>
<string name="error_copying_or_moving_file_to_same_path">%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.</string>
<string name="error_cannot_overwrite_a_different_file_type">Cannot overwrite %1$s while %2$s it from \"%3$s\" to \"%4$s\" since destination file type \"%5$s\" is different from source file type \"%6$s\".</string>
<string name="error_cannot_move_directory_to_sub_directory_of_itself">Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.</string>
<string name="error_file_still_exists_after_deleting">The %1$s still exists after deleting it from \"%2$s\".</string>
<string name="error_deleting_file_failed">Deleting %1$s at path \"%2$s\" failed.</string>
<string name="error_deleting_file_failed_with_exception">Deleting %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_clearing_directory_failed_with_exception">Clearing %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_reading_string_to_file_failed_with_exception">Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_writing_string_to_file_failed_with_exception">Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s</string>
<string name="error_unsupported_charset">Unsupported charset \"%1$s\"</string>
<string name="error_checking_if_charset_supported_failed">Checking if charset \"%1$s\" is suppoted failed.\nException: %2$s</string>
<string name="error_invalid_file_permissions_string_to_check">The file permission string to check is invalid.</string>
<string name="error_file_not_readable">The %1$s at path is not readable. Permission Denied.</string>
<string name="error_file_not_writable">The %1$s at path is not writable. Permission Denied.</string>
<string name="error_file_not_executable">The %1$s at path is not executable. Permission Denied.</string>
<!-- PermissionUtils -->
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
<string name="error_display_over_other_apps_permission_not_granted">&TERMUX_APP_NAME; requires \"Display over other apps\" permission to start terminal sessions from background on Android >= 10. Grants it from Settings -> Apps -> &TERMUX_APP_NAME; -> Advanced</string>
<!-- ShareUtils -->
<string name="title_share_with">Share With</string>
<!-- TermuxUtils -->
<string name="msg_report_issue">If you think this report should be reported, then copy its text from the options menu (3-dots on top right) and post an issue on one of the following links at which the report belongs at.</string>
<!-- Log Level -->
<string name="log_level_title">Log Level</string>
<string name="log_level_off">"Off"</string>
<string name="log_level_normal">"Normal"</string>
<string name="log_level_debug">"Debug"</string>
<string name="log_level_verbose">"Verbose"</string>
<string name="log_level_unknown">"*Unknown*"</string>
<string name="log_level_value">Logcat log level set to \"%1$s\"</string>
</resources>