Repo created
This commit is contained in:
parent
d22b8dc57b
commit
24b567c524
271 changed files with 39630 additions and 2 deletions
1
termux-shared/.gitignore
vendored
Normal file
1
termux-shared/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/build
|
||||
65
termux-shared/LICENSE.md
Normal file
65
termux-shared/LICENSE.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
The `termux-shared` library is released under [GPLv3 only](https://www.gnu.org/licenses/gpl-3.0.html) license.
|
||||
|
||||
### Exceptions
|
||||
|
||||
#### [MIT License](https://opensource.org/licenses/MIT)
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/TermuxConstants.java`](src/main/java/com/termux/shared/termux/TermuxConstants.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java`](src/main/java/com/termux/shared/settings/properties/TermuxPropertyConstants.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/activities/*`](src/main/java/com/termux/shared/activities).
|
||||
|
||||
- [`src/main/java/com/termux/shared/crash/CrashHandler.java`](src/main/java/com/termux/shared/crash/CrashHandler.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/data/DataUtils.java`](src/main/java/com/termux/shared/data/DataUtils.java).
|
||||
- [`src/main/java/com/termux/shared/data/IntentUtils.java`](src/main/java/com/termux/shared/data/IntentUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileType.java`](src/main/java/com/termux/shared/file/filesystem/FileType.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/FileTypes.java`](src/main/java/com/termux/shared/file/filesystem/FileTypes.java).
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java`](src/main/java/com/termux/shared/file/filesystem/NativeDispatcher.java).
|
||||
- [`src/main/java/com/termux/shared/file/tests/FileUtilsTests.java`](src/main/java/com/termux/shared/file/tests/FileUtilsTests.java).
|
||||
- [`src/main/java/com/termux/shared/file/FileUtils.java`](src/main/java/com/termux/shared/file/FileUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/interact/ShareUtils.java`](src/main/java/com/termux/shared/interact/ShareUtils.java).
|
||||
- [`src/main/java/com/termux/shared/interact/MessageDialogUtils.java`](src/main/java/com/termux/shared/interact/MessageDialogUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/logger/Logger.java`](src/main/java/com/termux/shared/logger/Logger.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/markdown/MarkdownUtils.java`](src/main/java/com/termux/shared/markdown/MarkdownUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/models/*`](src/main/java/com/termux/shared/models).
|
||||
|
||||
- [`src/main/java/com/termux/shared/notification/NotificationUtils.java`](src/main/java/com/termux/shared/notification/NotificationUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java`](src/main/java/com/termux/shared/settings/preferences/SharedPreferenceUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java`](src/main/java/com/termux/shared/settings/properties/SharedPropertiesParser.java).
|
||||
- [`src/main/java/com/termux/shared/settings/properties/SharedProperties.java`](src/main/java/com/termux/shared/settings/properties/SharedProperties.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/ResultSender.java`](src/main/java/com/termux/shared/shell/ResultSender.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java`](src/main/java/com/termux/shared/shell/ShellEnvironmentClient.java).
|
||||
- [`src/main/java/com/termux/shared/shell/ShellUtils.java`](src/main/java/com/termux/shared/shell/ShellUtils.java).
|
||||
- [`src/main/java/com/termux/shared/shell/TermuxTask.java`](src/main/java/com/termux/shared/shell/TermuxTask.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/termux/AndroidUtils.java`](src/main/java/com/termux/shared/termux/AndroidUtils.java).
|
||||
|
||||
- [`src/main/java/com/termux/shared/view/KeyboardUtils.java`](src/main/java/com/termux/shared/view/KeyboardUtils.java).
|
||||
- [`src/main/java/com/termux/shared/view/ViewUtils.java`](src/main/java/com/termux/shared/view/ViewUtils.java).
|
||||
|
||||
- [`src/main/res/drawable/*`](src/main/res/drawable).
|
||||
- [`src/main/res/layout/*`](src/main/res/layout).
|
||||
- [`src/main/res/menu/*`](src/main/res/menu).
|
||||
- [`src/main/res/values/*`](src/main/res/values).
|
||||
##
|
||||
|
||||
|
||||
#### [GPLv2 only with "Classpath" exception](https://openjdk.java.net/legal/gplv2+ce.html)
|
||||
|
||||
- [`src/main/java/com/termux/shared/file/filesystem/*`](src/main/java/com/termux/shared/file/filesystem) files that use code from [libcore/ojluni](https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:libcore/ojluni/).
|
||||
##
|
||||
|
||||
|
||||
#### [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
- [`src/main/java/com/termux/shared/shell/StreamGobbler.java`](src/main/java/com/termux/shared/shell/StreamGobbler.java) uses code from [libsuperuser ](https://github.com/Chainfire/libsuperuser).
|
||||
##
|
||||
74
termux-shared/build.gradle
Normal file
74
termux-shared/build.gradle
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'maven-publish'
|
||||
|
||||
android {
|
||||
compileSdkVersion project.properties.compileSdkVersion.toInteger()
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation "androidx.annotation:annotation:1.3.0"
|
||||
implementation "androidx.core:core:1.6.0"
|
||||
implementation 'com.google.android.material:material:1.4.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"
|
||||
implementation "org.lsposed.hiddenapibypass:hiddenapibypass:6.1"
|
||||
|
||||
// Do not increment version higher than 1.0.0-alpha09 since it will break ViewUtils and needs to be looked into
|
||||
// noinspection GradleDependency
|
||||
implementation "androidx.window:window:1.0.0-alpha09"
|
||||
|
||||
// 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"
|
||||
|
||||
implementation project(":terminal-view")
|
||||
}
|
||||
|
||||
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.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
}
|
||||
|
||||
task sourceJar(type: Jar) {
|
||||
from android.sourceSets.main.java.srcDirs
|
||||
classifier "sources"
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
publishing {
|
||||
publications {
|
||||
// Creates a Maven publication called "release".
|
||||
release(MavenPublication) {
|
||||
from components.release
|
||||
groupId = 'com.termux'
|
||||
artifactId = 'termux-shared'
|
||||
version = '0.118.0'
|
||||
artifact(sourceJar)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
termux-shared/proguard-rules.pro
vendored
Normal file
10
termux-shared/proguard-rules.pro
vendored
Normal 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
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
5
termux-shared/src/main/AndroidManifest.xml
Normal file
5
termux-shared/src/main/AndroidManifest.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.termux.shared">
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
</manifest>
|
||||
|
|
@ -0,0 +1,472 @@
|
|||
package com.termux.shared.activities;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.file.filesystem.FileType;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.models.ReportInfo;
|
||||
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import io.noties.markwon.Markwon;
|
||||
import io.noties.markwon.recycler.MarkwonAdapter;
|
||||
import io.noties.markwon.recycler.SimpleEntry;
|
||||
|
||||
/**
|
||||
* An activity to show reports in markdown format as per CommonMark spec based on config passed as {@link ReportInfo}.
|
||||
* Add Following to `AndroidManifest.xml` to use in an app:
|
||||
* {@code `<activity android:name="com.termux.shared.activities.ReportActivity" android:theme="@style/Theme.AppCompat.TermuxReportActivity" android:documentLaunchMode="intoExisting" />` }
|
||||
* and
|
||||
* {@code `<receiver android:name="com.termux.shared.activities.ReportActivity$ReportActivityBroadcastReceiver" android:exported="false" />` }
|
||||
* Receiver **must not** be `exported="true"`!!!
|
||||
*
|
||||
* Also make an incremental call to {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)}
|
||||
* in the app to cleanup cached files.
|
||||
*/
|
||||
public class ReportActivity extends AppCompatActivity {
|
||||
|
||||
private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
|
||||
private static final String ACTION_DELETE_REPORT_INFO_OBJECT_FILE = CLASS_NAME + ".ACTION_DELETE_REPORT_INFO_OBJECT_FILE";
|
||||
|
||||
private static final String EXTRA_REPORT_INFO_OBJECT = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT";
|
||||
private static final String EXTRA_REPORT_INFO_OBJECT_FILE_PATH = CLASS_NAME + ".EXTRA_REPORT_INFO_OBJECT_FILE_PATH";
|
||||
|
||||
private static final String CACHE_DIR_BASENAME = "report_activity";
|
||||
private static final String CACHE_FILE_BASENAME_PREFIX = "report_info_";
|
||||
|
||||
public static final int REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE = 1000;
|
||||
|
||||
public static final int ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES = 1000 * 1024; // 1MB
|
||||
|
||||
private ReportInfo mReportInfo;
|
||||
private String mReportInfoFilePath;
|
||||
private String mReportActivityMarkdownString;
|
||||
private Bundle mBundle;
|
||||
|
||||
private static final String LOG_TAG = "ReportActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
|
||||
setContentView(R.layout.activity_report);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
mBundle = null;
|
||||
Intent intent = getIntent();
|
||||
if (intent != null)
|
||||
mBundle = intent.getExtras();
|
||||
else if (savedInstanceState != null)
|
||||
mBundle = savedInstanceState;
|
||||
|
||||
updateUI();
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
Logger.logVerbose(LOG_TAG, "onNewIntent");
|
||||
|
||||
setIntent(intent);
|
||||
|
||||
if (intent != null) {
|
||||
deleteReportInfoFile(this, mReportInfoFilePath);
|
||||
mBundle = intent.getExtras();
|
||||
updateUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUI() {
|
||||
|
||||
if (mBundle == null) {
|
||||
finish(); return;
|
||||
}
|
||||
|
||||
mReportInfo = null;
|
||||
mReportInfoFilePath = null;
|
||||
|
||||
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
|
||||
mReportInfoFilePath = mBundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH);
|
||||
Logger.logVerbose(LOG_TAG, ReportInfo.class.getSimpleName() + " serialized object will be read from file at path \"" + mReportInfoFilePath + "\"");
|
||||
if (mReportInfoFilePath != null) {
|
||||
try {
|
||||
FileUtils.ReadSerializableObjectResult result = FileUtils.readSerializableObjectFromFile(ReportInfo.class.getSimpleName(), mReportInfoFilePath, ReportInfo.class, false);
|
||||
if (result.error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, result.error.toString());
|
||||
Logger.showToast(this, Error.getMinimalErrorString(result.error), true);
|
||||
finish(); return;
|
||||
} else {
|
||||
if (result.serializableObject != null)
|
||||
mReportInfo = (ReportInfo) result.serializableObject;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.logErrorAndShowToast(this, LOG_TAG, e.getMessage());
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failure while getting " + ReportInfo.class.getSimpleName() + " serialized object from file at path \"" + mReportInfoFilePath + "\"", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mReportInfo = (ReportInfo) mBundle.getSerializable(EXTRA_REPORT_INFO_OBJECT);
|
||||
}
|
||||
|
||||
if (mReportInfo == null) {
|
||||
finish(); return;
|
||||
}
|
||||
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
if (mReportInfo.reportTitle != null)
|
||||
actionBar.setTitle(mReportInfo.reportTitle);
|
||||
else
|
||||
actionBar.setTitle(TermuxConstants.TERMUX_APP_NAME + " App Report");
|
||||
}
|
||||
|
||||
|
||||
RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||
|
||||
final Markwon markwon = MarkdownUtils.getRecyclerMarkwonBuilder(this);
|
||||
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builderTextViewIsRoot(R.layout.markdown_adapter_node_default)
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.markdown_adapter_node_code_block, R.id.code_text_view))
|
||||
.build();
|
||||
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
generateReportActivityMarkdownString();
|
||||
adapter.setMarkdown(markwon, mReportActivityMarkdownString);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (mBundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
|
||||
outState.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, mReportInfoFilePath);
|
||||
} else {
|
||||
outState.putSerializable(EXTRA_REPORT_INFO_OBJECT, mReportInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
Logger.logVerbose(LOG_TAG, "onDestroy");
|
||||
|
||||
deleteReportInfoFile(this, mReportInfoFilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_report, menu);
|
||||
|
||||
if (mReportInfo.reportSaveFilePath == null) {
|
||||
MenuItem item = menu.findItem(R.id.menu_item_save_report_to_file);
|
||||
if (item != null)
|
||||
item.setEnabled(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
// Remove activity from recents menu on back button press
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.menu_item_share_report) {
|
||||
ShareUtils.shareText(this, getString(R.string.title_report_text), ReportInfo.getReportInfoMarkdownString(mReportInfo));
|
||||
} else if (id == R.id.menu_item_copy_report) {
|
||||
ShareUtils.copyTextToClipboard(this, ReportInfo.getReportInfoMarkdownString(mReportInfo), null);
|
||||
} else if (id == R.id.menu_item_save_report_to_file) {
|
||||
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
|
||||
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
|
||||
true, REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission granted by user on request.");
|
||||
if (requestCode == REQUEST_GRANT_STORAGE_PERMISSION_FOR_SAVE_FILE) {
|
||||
ShareUtils.saveTextToFile(this, mReportInfo.reportSaveFileLabel,
|
||||
mReportInfo.reportSaveFilePath, ReportInfo.getReportInfoMarkdownString(mReportInfo),
|
||||
true, -1);
|
||||
}
|
||||
} else {
|
||||
Logger.logInfo(LOG_TAG, "Storage permission denied by user on request.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the markdown {@link String} to be shown in {@link ReportActivity}.
|
||||
*/
|
||||
private void generateReportActivityMarkdownString() {
|
||||
// We need to reduce chances of OutOfMemoryError happening so reduce new allocations and
|
||||
// do not keep output of getReportInfoMarkdownString in memory
|
||||
StringBuilder reportString = new StringBuilder();
|
||||
|
||||
if (mReportInfo.reportStringPrefix != null)
|
||||
reportString.append(mReportInfo.reportStringPrefix);
|
||||
|
||||
String reportMarkdownString = ReportInfo.getReportInfoMarkdownString(mReportInfo);
|
||||
int reportMarkdownStringSize = reportMarkdownString.getBytes().length;
|
||||
boolean truncated = false;
|
||||
if (reportMarkdownStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
|
||||
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string size " + reportMarkdownStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
|
||||
reportString.append(DataUtils.getTruncatedCommandOutput(reportMarkdownString, ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, true));
|
||||
truncated = true;
|
||||
} else {
|
||||
reportString.append(reportMarkdownString);
|
||||
}
|
||||
|
||||
// Free reference
|
||||
reportMarkdownString = null;
|
||||
|
||||
if (mReportInfo.reportStringSuffix != null)
|
||||
reportString.append(mReportInfo.reportStringSuffix);
|
||||
|
||||
int reportStringSize = reportString.length();
|
||||
if (reportStringSize > ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES) {
|
||||
// This may break markdown formatting
|
||||
Logger.logVerbose(LOG_TAG, mReportInfo.reportTitle + " report string total size " + reportStringSize + " is greater than " + ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES + " and will be truncated");
|
||||
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) +
|
||||
DataUtils.getTruncatedCommandOutput(reportString.toString(), ACTIVITY_TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
|
||||
} else if (truncated) {
|
||||
mReportActivityMarkdownString = this.getString(R.string.msg_report_truncated) + reportString.toString();
|
||||
} else {
|
||||
mReportActivityMarkdownString = reportString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static class NewInstanceResult {
|
||||
/** An intent that can be used to start the {@link ReportActivity}. */
|
||||
public Intent contentIntent;
|
||||
/** An intent that can should be adding as the {@link android.app.Notification#deleteIntent}
|
||||
* by a call to {@link android.app.PendingIntent#getBroadcast(Context, int, Intent, int)}
|
||||
* so that {@link ReportActivityBroadcastReceiver} can do cleanup of {@link #EXTRA_REPORT_INFO_OBJECT_FILE_PATH}. */
|
||||
public Intent deleteIntent;
|
||||
|
||||
NewInstanceResult(Intent contentIntent, Intent deleteIntent) {
|
||||
this.contentIntent = contentIntent;
|
||||
this.deleteIntent = deleteIntent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the {@link ReportActivity}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
|
||||
*/
|
||||
public static void startReportActivity(@NonNull final Context context, @NonNull ReportInfo reportInfo) {
|
||||
NewInstanceResult result = newInstance(context, reportInfo);
|
||||
if (result.contentIntent == null) return;
|
||||
context.startActivity(result.contentIntent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content and delete intents for the {@link ReportActivity} that can be used to start it
|
||||
* and do cleanup.
|
||||
*
|
||||
* If {@link ReportInfo} size is too large, then a TransactionTooLargeException will be thrown
|
||||
* so its object may be saved to a file in the {@link Context#getCacheDir()}. Then when activity
|
||||
* starts, its read back and the file is deleted in {@link #onDestroy()}.
|
||||
* Note that files may still be left if {@link #onDestroy()} is not called or doesn't finish.
|
||||
* A separate cleanup routine is implemented from that case by
|
||||
* {@link #deleteReportInfoFilesOlderThanXDays(Context, int, boolean)} which should be called
|
||||
* incrementally or at app startup.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param reportInfo The {@link ReportInfo} containing info that needs to be displayed.
|
||||
* @return Returns {@link NewInstanceResult}.
|
||||
*/
|
||||
@NonNull
|
||||
public static NewInstanceResult newInstance(@NonNull final Context context, @NonNull final ReportInfo reportInfo) {
|
||||
|
||||
long size = DataUtils.getSerializedSize(reportInfo);
|
||||
if (size > DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES) {
|
||||
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
|
||||
String reportInfoFilePath = reportInfoDirectoryPath + "/" + CACHE_FILE_BASENAME_PREFIX + reportInfo.reportTimestamp;
|
||||
Logger.logVerbose(LOG_TAG, reportInfo.reportTitle + " " + ReportInfo.class.getSimpleName() + " serialized object size " + size + " is greater than " + DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES + " and it will be written to file at path \"" + reportInfoFilePath + "\"");
|
||||
Error error = FileUtils.writeSerializableObjectToFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, reportInfo);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
Logger.showToast(context, Error.getMinimalErrorString(error), true);
|
||||
return new NewInstanceResult(null, null);
|
||||
}
|
||||
|
||||
return new NewInstanceResult(createContentIntent(context, null, reportInfoFilePath),
|
||||
createDeleteIntent(context, reportInfoFilePath));
|
||||
} else {
|
||||
return new NewInstanceResult(createContentIntent(context, reportInfo, null),
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private static Intent createContentIntent(@NonNull final Context context, final ReportInfo reportInfo, final String reportInfoFilePath) {
|
||||
Intent intent = new Intent(context, ReportActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
|
||||
if (reportInfoFilePath != null) {
|
||||
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
|
||||
} else {
|
||||
bundle.putSerializable(EXTRA_REPORT_INFO_OBJECT, reportInfo);
|
||||
}
|
||||
|
||||
intent.putExtras(bundle);
|
||||
|
||||
// Note that ReportActivity should have `documentLaunchMode="intoExisting"` set in `AndroidManifest.xml`
|
||||
// which has equivalent behaviour to FLAG_ACTIVITY_NEW_DOCUMENT.
|
||||
// FLAG_ACTIVITY_SINGLE_TOP must also be passed for onNewIntent to be called.
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
private static Intent createDeleteIntent(@NonNull final Context context, final String reportInfoFilePath) {
|
||||
if (reportInfoFilePath == null) return null;
|
||||
|
||||
Intent intent = new Intent(context, ReportActivityBroadcastReceiver.class);
|
||||
intent.setAction(ACTION_DELETE_REPORT_INFO_OBJECT_FILE);
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH, reportInfoFilePath);
|
||||
intent.putExtras(bundle);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@NotNull
|
||||
private static String getReportInfoDirectoryPath(Context context) {
|
||||
// Canonicalize to solve /data/data and /data/user/0 issues when comparing with reportInfoFilePath
|
||||
return FileUtils.getCanonicalPath(context.getCacheDir().getAbsolutePath(), null) + "/" + CACHE_DIR_BASENAME;
|
||||
}
|
||||
|
||||
private static void deleteReportInfoFile(Context context, String reportInfoFilePath) {
|
||||
if (context == null || reportInfoFilePath == null) return;
|
||||
|
||||
// Extra protection for mainly if someone set `exported="true"` for ReportActivityBroadcastReceiver
|
||||
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
|
||||
reportInfoFilePath = FileUtils.getCanonicalPath(reportInfoFilePath, null);
|
||||
if(!reportInfoFilePath.equals(reportInfoDirectoryPath) && reportInfoFilePath.startsWith(reportInfoDirectoryPath + "/")) {
|
||||
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\"");
|
||||
Error error = FileUtils.deleteRegularFile(ReportInfo.class.getSimpleName(), reportInfoFilePath, true);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
}
|
||||
} else {
|
||||
Logger.logError(LOG_TAG, "Not deleting " + ReportInfo.class.getSimpleName() + " serialized object file at path \"" + reportInfoFilePath + "\" since its not under \"" + reportInfoDirectoryPath + "\"");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete {@link ReportInfo} serialized object files from cache older than x days. If a notification
|
||||
* has still not been opened after x days that's using a PendingIntent to ReportActivity, then
|
||||
* opening the notification will throw a file not found error, so choose days value appropriately
|
||||
* or check if a notification is still active if tracking notification ids.
|
||||
* The {@link Context} object passed must be of the same package with which {@link #newInstance(Context, ReportInfo)}
|
||||
* was called since a call to {@link Context#getCacheDir()} is made.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param days The x amount of days before which files should be deleted. This must be `>=0`.
|
||||
* @param isSynchronous If set to {@code true}, then the command will be executed in the
|
||||
* caller thread and results returned synchronously.
|
||||
* If set to {@code false}, then a new thread is started run the commands
|
||||
* asynchronously in the background and control is returned to the caller thread.
|
||||
* @return Returns the {@code error} if deleting was not successful, otherwise {@code null}.
|
||||
*/
|
||||
public static Error deleteReportInfoFilesOlderThanXDays(@NonNull final Context context, int days, final boolean isSynchronous) {
|
||||
if (isSynchronous) {
|
||||
return deleteReportInfoFilesOlderThanXDaysInner(context, days);
|
||||
} else {
|
||||
new Thread() { public void run() {
|
||||
Error error = deleteReportInfoFilesOlderThanXDaysInner(context, days);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
}
|
||||
}}.start();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Error deleteReportInfoFilesOlderThanXDaysInner(@NonNull final Context context, int days) {
|
||||
// Only regular files are deleted and subdirectories are not checked
|
||||
String reportInfoDirectoryPath = getReportInfoDirectoryPath(context);
|
||||
Logger.logVerbose(LOG_TAG, "Deleting " + ReportInfo.class.getSimpleName() + " serialized object files under directory path \"" + reportInfoDirectoryPath + "\" older than " + days + " days");
|
||||
return FileUtils.deleteFilesOlderThanXDays(ReportInfo.class.getSimpleName(), reportInfoDirectoryPath, null, days, true, FileType.REGULAR.getValue());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The {@link BroadcastReceiver} for {@link ReportActivity} that currently does cleanup when
|
||||
* {@link android.app.Notification#deleteIntent} is called. It must be registered in `AndroidManifest.xml`.
|
||||
*/
|
||||
public static class ReportActivityBroadcastReceiver extends BroadcastReceiver {
|
||||
private static final String LOG_TAG = "ReportActivityBroadcastReceiver";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent == null) return;
|
||||
|
||||
String action = intent.getAction();
|
||||
Logger.logVerbose(LOG_TAG, "onReceive: \"" + action + "\" action");
|
||||
|
||||
if (ACTION_DELETE_REPORT_INFO_OBJECT_FILE.equals(action)) {
|
||||
Bundle bundle = intent.getExtras();
|
||||
if (bundle == null) return;
|
||||
if (bundle.containsKey(EXTRA_REPORT_INFO_OBJECT_FILE_PATH)) {
|
||||
deleteReportInfoFile(context, bundle.getString(EXTRA_REPORT_INFO_OBJECT_FILE_PATH));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,278 @@
|
|||
package com.termux.shared.activities;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
import com.termux.shared.interact.ShareUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.models.TextIOInfo;
|
||||
import com.termux.shared.view.KeyboardUtils;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* An activity to edit or view text based on config passed as {@link TextIOInfo}.
|
||||
*
|
||||
* Add Following to `AndroidManifest.xml` to use in an app:
|
||||
*
|
||||
* {@code ` <activity android:name="com.termux.shared.activities.TextIOActivity" android:theme="@style/Theme.AppCompat.TermuxTextIOActivity" />` }
|
||||
*/
|
||||
public class TextIOActivity extends AppCompatActivity {
|
||||
|
||||
private static final String CLASS_NAME = ReportActivity.class.getCanonicalName();
|
||||
public static final String EXTRA_TEXT_IO_INFO_OBJECT = CLASS_NAME + ".EXTRA_TEXT_IO_INFO_OBJECT";
|
||||
|
||||
private TextView mTextIOLabel;
|
||||
private View mTextIOLabelSeparator;
|
||||
private EditText mTextIOText;
|
||||
private HorizontalScrollView mTextIOHorizontalScrollView;
|
||||
private LinearLayout mTextIOTextLinearLayout;
|
||||
private TextView mTextIOTextCharacterUsage;
|
||||
|
||||
private TextIOInfo mTextIOInfo;
|
||||
private Bundle mBundle;
|
||||
|
||||
private static final String LOG_TAG = "TextIOActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Logger.logVerbose(LOG_TAG, "onCreate");
|
||||
|
||||
setContentView(R.layout.activity_text_io);
|
||||
|
||||
mTextIOLabel = findViewById(R.id.text_io_label);
|
||||
mTextIOLabelSeparator = findViewById(R.id.text_io_label_separator);
|
||||
mTextIOText = findViewById(R.id.text_io_text);
|
||||
mTextIOHorizontalScrollView = findViewById(R.id.text_io_horizontal_scroll_view);
|
||||
mTextIOTextLinearLayout = findViewById(R.id.text_io_text_linear_layout);
|
||||
mTextIOTextCharacterUsage = findViewById(R.id.text_io_text_character_usage);
|
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
if (toolbar != null) {
|
||||
setSupportActionBar(toolbar);
|
||||
}
|
||||
|
||||
mBundle = null;
|
||||
Intent intent = getIntent();
|
||||
if (intent != null)
|
||||
mBundle = intent.getExtras();
|
||||
else if (savedInstanceState != null)
|
||||
mBundle = savedInstanceState;
|
||||
|
||||
updateUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
Logger.logVerbose(LOG_TAG, "onNewIntent");
|
||||
|
||||
// Views must be re-created since different configs for isEditingTextDisabled() and
|
||||
// isHorizontallyScrollable() will not work or at least reliably
|
||||
finish();
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void updateUI() {
|
||||
if (mBundle == null) {
|
||||
finish(); return;
|
||||
}
|
||||
|
||||
mTextIOInfo = (TextIOInfo) mBundle.getSerializable(EXTRA_TEXT_IO_INFO_OBJECT);
|
||||
if (mTextIOInfo == null) {
|
||||
finish(); return;
|
||||
}
|
||||
|
||||
final ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
if (mTextIOInfo.getTitle() != null)
|
||||
actionBar.setTitle(mTextIOInfo.getTitle());
|
||||
else
|
||||
actionBar.setTitle("Text Input");
|
||||
|
||||
if (mTextIOInfo.shouldShowBackButtonInActionBar()) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
mTextIOLabel.setVisibility(View.GONE);
|
||||
mTextIOLabelSeparator.setVisibility(View.GONE);
|
||||
if (mTextIOInfo.isLabelEnabled()) {
|
||||
mTextIOLabel.setVisibility(View.VISIBLE);
|
||||
mTextIOLabelSeparator.setVisibility(View.VISIBLE);
|
||||
mTextIOLabel.setText(mTextIOInfo.getLabel());
|
||||
mTextIOLabel.setFilters(new InputFilter[] { new InputFilter.LengthFilter(TextIOInfo.LABEL_SIZE_LIMIT_IN_BYTES) });
|
||||
mTextIOLabel.setTextSize(mTextIOInfo.getLabelSize());
|
||||
mTextIOLabel.setTextColor(mTextIOInfo.getLabelColor());
|
||||
mTextIOLabel.setTypeface(Typeface.create(mTextIOInfo.getLabelTypeFaceFamily(), mTextIOInfo.getLabelTypeFaceStyle()));
|
||||
}
|
||||
|
||||
|
||||
if (mTextIOInfo.isHorizontallyScrollable()) {
|
||||
mTextIOHorizontalScrollView.setEnabled(true);
|
||||
mTextIOText.setHorizontallyScrolling(true);
|
||||
} else {
|
||||
// Remove mTextIOHorizontalScrollView and add mTextIOText in its place
|
||||
ViewGroup parent = (ViewGroup) mTextIOHorizontalScrollView.getParent();
|
||||
if (parent != null && parent.indexOfChild(mTextIOText) < 0) {
|
||||
ViewGroup.LayoutParams params = mTextIOHorizontalScrollView.getLayoutParams();
|
||||
int index = parent.indexOfChild(mTextIOHorizontalScrollView);
|
||||
mTextIOTextLinearLayout.removeAllViews();
|
||||
mTextIOHorizontalScrollView.removeAllViews();
|
||||
parent.removeView(mTextIOHorizontalScrollView);
|
||||
parent.addView(mTextIOText, index, params);
|
||||
mTextIOText.setHorizontallyScrolling(false);
|
||||
}
|
||||
}
|
||||
|
||||
mTextIOText.setText(mTextIOInfo.getText());
|
||||
mTextIOText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(mTextIOInfo.getTextLengthLimit()) });
|
||||
mTextIOText.setTextSize(mTextIOInfo.getTextSize());
|
||||
mTextIOText.setTextColor(mTextIOInfo.getTextColor());
|
||||
mTextIOText.setTypeface(Typeface.create(mTextIOInfo.getTextTypeFaceFamily(), mTextIOInfo.getTextTypeFaceStyle()));
|
||||
|
||||
// setTextIsSelectable must be called after changing KeyListener to regain focusability and selectivity
|
||||
if (mTextIOInfo.isEditingTextDisabled()) {
|
||||
mTextIOText.setCursorVisible(false);
|
||||
mTextIOText.setKeyListener(null);
|
||||
mTextIOText.setTextIsSelectable(true);
|
||||
}
|
||||
|
||||
if (mTextIOInfo.shouldShowTextCharacterUsage()) {
|
||||
mTextIOTextCharacterUsage.setVisibility(View.VISIBLE);
|
||||
updateTextIOTextCharacterUsage(mTextIOInfo.getText());
|
||||
|
||||
mTextIOText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {}
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
if (editable != null)
|
||||
updateTextIOTextCharacterUsage(editable.toString());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mTextIOTextCharacterUsage.setVisibility(View.GONE);
|
||||
mTextIOText.addTextChangedListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTextIOInfoText() {
|
||||
if (mTextIOText != null)
|
||||
mTextIOInfo.setText(mTextIOText.getText().toString());
|
||||
}
|
||||
|
||||
private void updateTextIOTextCharacterUsage(String text) {
|
||||
if (text == null) text = "";
|
||||
if (mTextIOTextCharacterUsage != null)
|
||||
mTextIOTextCharacterUsage.setText(String.format(Locale.getDefault(), "%1$d/%2$d", text.length(), mTextIOInfo.getTextLengthLimit()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
updateTextIOInfoText();
|
||||
outState.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
final MenuInflater inflater = getMenuInflater();
|
||||
inflater.inflate(R.menu.menu_text_io, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||
String text = "";
|
||||
if (mTextIOText != null)
|
||||
text = mTextIOText.getText().toString();
|
||||
|
||||
int id = item.getItemId();
|
||||
if (id == android.R.id.home) {
|
||||
confirm();
|
||||
} if (id == R.id.menu_item_cancel) {
|
||||
cancel();
|
||||
} else if (id == R.id.menu_item_share_text) {
|
||||
ShareUtils.shareText(this, mTextIOInfo.getTitle(), text);
|
||||
} else if (id == R.id.menu_item_copy_text) {
|
||||
ShareUtils.copyTextToClipboard(this, text, null);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
confirm();
|
||||
}
|
||||
|
||||
/** Confirm current text and send it back to calling {@link Activity}. */
|
||||
private void confirm() {
|
||||
updateTextIOInfoText();
|
||||
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
|
||||
setResult(Activity.RESULT_OK, getResultIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
/** Cancel current text and notify calling {@link Activity}. */
|
||||
private void cancel() {
|
||||
KeyboardUtils.hideSoftKeyboard(this, mTextIOText);
|
||||
setResult(Activity.RESULT_CANCELED, getResultIntent());
|
||||
finish();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private Intent getResultIntent() {
|
||||
Intent intent = new Intent();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, mTextIOInfo);
|
||||
intent.putExtras(bundle);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Intent} that can be used to start the {@link TextIOActivity}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param textIOInfo The {@link TextIOInfo} containing info for the edit text.
|
||||
*/
|
||||
public static Intent newInstance(@NonNull final Context context, @NonNull final TextIOInfo textIOInfo) {
|
||||
Intent intent = new Intent(context, TextIOActivity.class);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(EXTRA_TEXT_IO_INFO_OBJECT, textIOInfo);
|
||||
intent.putExtras(bundle);
|
||||
return intent;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package com.termux.shared.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class SELinuxUtils {
|
||||
|
||||
public static final String ANDROID_OS_SELINUX_CLASS = "android.os.SELinux";
|
||||
|
||||
private static final String LOG_TAG = "SELinuxUtils";
|
||||
|
||||
/**
|
||||
* Gets the security context of the current process.
|
||||
*
|
||||
* @return Returns a {@link String} representing the security context of the current process.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getContext() {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security context of a given process id.
|
||||
*
|
||||
* @param pid The pid of process.
|
||||
* @return Returns a {@link String} representing the security context of the given pid.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getPidContext(int pid) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getPidContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, int.class);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null, pid).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the security context of a file object.
|
||||
*
|
||||
* @param path The pathname of the file object.
|
||||
* @return Returns a {@link String} representing the security context of the file.
|
||||
* This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getFileContext(@NonNull String path) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
String methodName = "getFileContext";
|
||||
try {
|
||||
@SuppressLint("PrivateApi") Class<?> clazz = Class.forName(ANDROID_OS_SELINUX_CLASS);
|
||||
Method method = ReflectionUtils.getDeclaredMethod(clazz, methodName, String.class);
|
||||
if (method == null) {
|
||||
Logger.logError(LOG_TAG, "Failed to get " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class");
|
||||
return null;
|
||||
}
|
||||
|
||||
return (String) ReflectionUtils.invokeMethod(method, null, path).value;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to call " + methodName + "() method of " + ANDROID_OS_SELINUX_CLASS + " class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
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.models.errors.Error;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
/**
|
||||
* Catches uncaught exceptions and logs them.
|
||||
*/
|
||||
public class CrashHandler implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private final Context mContext;
|
||||
private final CrashHandlerClient mCrashHandlerClient;
|
||||
private final Thread.UncaughtExceptionHandler defaultUEH;
|
||||
|
||||
private static final String LOG_TAG = "CrashUtils";
|
||||
|
||||
private CrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||
this.mContext = context;
|
||||
this.mCrashHandlerClient = crashHandlerClient;
|
||||
this.defaultUEH = Thread.getDefaultUncaughtExceptionHandler();
|
||||
}
|
||||
|
||||
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||
logCrash(mContext, mCrashHandlerClient, thread, throwable);
|
||||
defaultUEH.uncaughtException(thread, throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default uncaught crash handler of current thread to {@link CrashHandler}.
|
||||
*/
|
||||
public static void setCrashHandler(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient) {
|
||||
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof CrashHandler)) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(new CrashHandler(context, crashHandlerClient));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a crash in the crash log file at {@code crashlogFilePath}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param crashHandlerClient The {@link CrashHandlerClient} implementation.
|
||||
* @param thread The {@link Thread} in which the crash happened.
|
||||
* @param throwable The {@link Throwable} thrown for the crash.
|
||||
*/
|
||||
public static void logCrash(@NonNull final Context context, @NonNull final CrashHandlerClient crashHandlerClient, 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", AndroidUtils.getCurrentMilliSecondUTCTimeStamp(), "-"));
|
||||
reportString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Crash Message", throwable.getMessage(), "-"));
|
||||
reportString.append("\n\n").append(Logger.getStackTracesMarkdownString("Stacktrace", Logger.getStackTracesStringArray(throwable)));
|
||||
|
||||
String appInfoMarkdownString = crashHandlerClient.getAppInfoMarkdownString(context);
|
||||
if (appInfoMarkdownString != null && !appInfoMarkdownString.isEmpty())
|
||||
reportString.append("\n\n").append(appInfoMarkdownString);
|
||||
|
||||
reportString.append("\n\n").append(AndroidUtils.getDeviceInfoMarkdownString(context));
|
||||
|
||||
// Log report string to logcat
|
||||
Logger.logError(reportString.toString());
|
||||
|
||||
// Write report string to crash log file
|
||||
Error error = FileUtils.writeStringToFile("crash log", crashHandlerClient.getCrashLogFilePath(context),
|
||||
Charset.defaultCharset(), reportString.toString(), false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public interface CrashHandlerClient {
|
||||
|
||||
/**
|
||||
* Get crash log file path.
|
||||
*
|
||||
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||
* @return Should return the crash log file path.
|
||||
*/
|
||||
@NonNull
|
||||
String getCrashLogFilePath(Context context);
|
||||
|
||||
/**
|
||||
* Get app info markdown string to add to crash log.
|
||||
*
|
||||
* @param context The {@link Context} passed to {@link CrashHandler#CrashHandler(Context, CrashHandlerClient)}.
|
||||
* @return Should return app info markdown string.
|
||||
*/
|
||||
String getAppInfoMarkdownString(Context context);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.termux.shared.crash;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
public class TermuxCrashUtils implements CrashHandler.CrashHandlerClient {
|
||||
|
||||
/**
|
||||
* Set default uncaught crash handler of current thread to {@link CrashHandler} for Termux app
|
||||
* and its plugin to log crashes at {@link TermuxConstants#TERMUX_CRASH_LOG_FILE_PATH}.
|
||||
*/
|
||||
public static void setCrashHandler(@NonNull final Context context) {
|
||||
CrashHandler.setCrashHandler(context, new TermuxCrashUtils());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getCrashLogFilePath(Context context) {
|
||||
return TermuxConstants.TERMUX_CRASH_LOG_FILE_PATH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppInfoMarkdownString(Context context) {
|
||||
return TermuxUtils.getAppInfoMarkdownString(context, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,209 @@
|
|||
package com.termux.shared.data;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.ObjectOutputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
public class DataUtils {
|
||||
|
||||
/** Max safe limit of data size to prevent TransactionTooLargeException when transferring data
|
||||
* inside or to other apps via transactions. */
|
||||
public static final int TRANSACTION_SIZE_LIMIT_IN_BYTES = 100 * 1024; // 100KB
|
||||
|
||||
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
|
||||
|
||||
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, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace a sub string in each item of a {@link String[]}.
|
||||
*
|
||||
* @param array The {@link String[]} to replace in.
|
||||
* @param find The sub string to replace.
|
||||
* @param replace The sub string to replace with.
|
||||
*/
|
||||
public static void replaceSubStringsInStringArrayItems(String[] array, String find, String replace) {
|
||||
if(array == null || array.length == 0) return;
|
||||
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
array[i] = array[i].replace(find, replace);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 the {@code String} from an {@link Integer}.
|
||||
*
|
||||
* @param value The {@link Integer} value.
|
||||
* @param def The default {@link String} value.
|
||||
* @return Returns {@code value} if it is not {@code null}, otherwise returns {@code def}.
|
||||
*/
|
||||
public static String getStringFromInteger(Integer value, String def) {
|
||||
return (value == null) ? def : String.valueOf((int) value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code hex string} from a {@link byte[]}.
|
||||
*
|
||||
* @param bytes The {@link byte[]} value.
|
||||
* @return Returns the {@code hex string} value.
|
||||
*/
|
||||
public static String bytesToHex(byte[] bytes) {
|
||||
char[] hexChars = new char[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(@Nullable T object, @Nullable T def) {
|
||||
return (object == null) ? def : object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link String} itself if it is not {@code null} or empty, otherwise default.
|
||||
*
|
||||
* @param value The {@link String} to check.
|
||||
* @param def The default {@link String}.
|
||||
* @return Returns {@code value} if it is not {@code null} or empty, otherwise returns {@code def}.
|
||||
*/
|
||||
public static String getDefaultIfUnset(@Nullable String value, String def) {
|
||||
return (value == null || value.isEmpty()) ? def : value;
|
||||
}
|
||||
|
||||
/** Check if a string is null or empty. */
|
||||
public static boolean isNullOrEmpty(String string) {
|
||||
return string == null || string.isEmpty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Get size of a serializable object. */
|
||||
public static long getSerializedSize(Serializable object) {
|
||||
if (object == null) return 0;
|
||||
try {
|
||||
ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream();
|
||||
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteOutputStream);
|
||||
objectOutputStream.writeObject(object);
|
||||
objectOutputStream.flush();
|
||||
objectOutputStream.close();
|
||||
return byteOutputStream.toByteArray().length;
|
||||
} catch (Exception e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
package com.termux.shared.data;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class IntentUtils {
|
||||
|
||||
private static final String LOG_TAG = "IntentUtils";
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||
* is not set.
|
||||
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def, boolean throwExceptionIfNotSet) throws Exception {
|
||||
String value = getStringExtraIfSet(intent, key, def);
|
||||
if (value == null && throwExceptionIfNotSet)
|
||||
throw new Exception("The \"" + key + "\" key string value is null or empty");
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link String} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @return Returns the {@link String} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String getStringExtraIfSet(@NonNull Intent intent, String key, String def) {
|
||||
String value = intent.getStringExtra(key);
|
||||
if (value == null || value.isEmpty()) {
|
||||
if (def != null && !def.isEmpty())
|
||||
return def;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an {@link Integer} from an {@link Intent} stored as a {@link String} extra if its not
|
||||
* {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @return Returns the {@link Integer} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static Integer getIntegerExtraIfSet(@NonNull Intent intent, String key, Integer def) {
|
||||
try {
|
||||
String value = intent.getStringExtra(key);
|
||||
if (value == null || value.isEmpty()) {
|
||||
return def;
|
||||
}
|
||||
|
||||
return Integer.parseInt(value);
|
||||
}
|
||||
catch (Exception e) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @param throwExceptionIfNotSet If set to {@code true}, then an exception will be thrown if extra
|
||||
* is not set.
|
||||
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String[] getStringArrayExtraIfSet(@NonNull Intent intent, String key, String[] def, boolean throwExceptionIfNotSet) throws Exception {
|
||||
String[] value = getStringArrayExtraIfSet(intent, key, def);
|
||||
if (value == null && throwExceptionIfNotSet)
|
||||
throw new Exception("The \"" + key + "\" key string array is null or empty");
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link String[]} extra from an {@link Intent} if its not {@code null} or empty.
|
||||
*
|
||||
* @param intent The {@link Intent} to get the extra from.
|
||||
* @param key The {@link String} key name.
|
||||
* @param def The default value if extra is not set.
|
||||
* @return Returns the {@link String[]} extra if set, otherwise {@code null}.
|
||||
*/
|
||||
public static String[] getStringArrayExtraIfSet(Intent intent, String key, String[] def) {
|
||||
String[] value = intent.getStringArrayExtra(key);
|
||||
if (value == null || value.length == 0) {
|
||||
if (def != null && def.length != 0)
|
||||
return def;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public static String getIntentString(Intent intent) {
|
||||
if (intent == null) return null;
|
||||
|
||||
return intent.toString() + "\n" + getBundleString(intent.getExtras());
|
||||
}
|
||||
|
||||
public static String getBundleString(Bundle bundle) {
|
||||
if (bundle == null || bundle.size() == 0) return "Bundle[]";
|
||||
|
||||
StringBuilder bundleString = new StringBuilder("Bundle[\n");
|
||||
boolean first = true;
|
||||
for (String key : bundle.keySet()) {
|
||||
if (!first)
|
||||
bundleString.append("\n");
|
||||
|
||||
bundleString.append(key).append(": `");
|
||||
|
||||
Object value = bundle.get(key);
|
||||
if (value instanceof int[]) {
|
||||
bundleString.append(Arrays.toString((int[]) value));
|
||||
} else if (value instanceof byte[]) {
|
||||
bundleString.append(Arrays.toString((byte[]) value));
|
||||
} else if (value instanceof boolean[]) {
|
||||
bundleString.append(Arrays.toString((boolean[]) value));
|
||||
} else if (value instanceof short[]) {
|
||||
bundleString.append(Arrays.toString((short[]) value));
|
||||
} else if (value instanceof long[]) {
|
||||
bundleString.append(Arrays.toString((long[]) value));
|
||||
} else if (value instanceof float[]) {
|
||||
bundleString.append(Arrays.toString((float[]) value));
|
||||
} else if (value instanceof double[]) {
|
||||
bundleString.append(Arrays.toString((double[]) value));
|
||||
} else if (value instanceof String[]) {
|
||||
bundleString.append(Arrays.toString((String[]) value));
|
||||
} else if (value instanceof CharSequence[]) {
|
||||
bundleString.append(Arrays.toString((CharSequence[]) value));
|
||||
} else if (value instanceof Parcelable[]) {
|
||||
bundleString.append(Arrays.toString((Parcelable[]) value));
|
||||
} else if (value instanceof Bundle) {
|
||||
bundleString.append(getBundleString((Bundle) value));
|
||||
} else {
|
||||
bundleString.append(value);
|
||||
}
|
||||
|
||||
bundleString.append("`");
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
bundleString.append("\n]");
|
||||
return bundleString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
104
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
104
termux-shared/src/main/java/com/termux/shared/data/UrlUtils.java
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package com.termux.shared.data;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class UrlUtils {
|
||||
|
||||
public static Pattern URL_MATCH_REGEX;
|
||||
|
||||
public static Pattern getUrlMatchRegex() {
|
||||
if (URL_MATCH_REGEX != null) return URL_MATCH_REGEX;
|
||||
|
||||
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("gemini|"); // The Gemini 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(")");
|
||||
|
||||
URL_MATCH_REGEX = Pattern.compile(
|
||||
regex_sb.toString(),
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
|
||||
|
||||
return URL_MATCH_REGEX;
|
||||
}
|
||||
|
||||
public static LinkedHashSet<CharSequence> extractUrls(String text) {
|
||||
LinkedHashSet<CharSequence> urlSet = new LinkedHashSet<>();
|
||||
Matcher matcher = getUrlMatchRegex().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;
|
||||
}
|
||||
|
||||
}
|
||||
1858
termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
Normal file
1858
termux-shared/src/main/java/com/termux/shared/file/FileUtils.java
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,377 @@
|
|||
package com.termux.shared.file;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.models.errors.FileUtilsErrno;
|
||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.termux.TermuxUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxFileUtils {
|
||||
|
||||
private static final String LOG_TAG = "TermuxFileUtils";
|
||||
|
||||
/**
|
||||
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
||||
*
|
||||
* @param paths The {@code paths} to expand.
|
||||
* @return Returns the {@code expand paths}.
|
||||
*/
|
||||
public static List<String> getExpandedTermuxPaths(List<String> paths) {
|
||||
if (paths == null) return null;
|
||||
List<String> expandedPaths = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
expandedPaths.add(getExpandedTermuxPath(paths.get(i)));
|
||||
}
|
||||
|
||||
return expandedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace "$PREFIX/" or "~/" prefix with termux absolute paths.
|
||||
*
|
||||
* @param path The {@code path} to expand.
|
||||
* @return Returns the {@code expand path}.
|
||||
*/
|
||||
public static String getExpandedTermuxPath(String path) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^\\$PREFIX$", TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
path = path.replaceAll("^\\$PREFIX/", TermuxConstants.TERMUX_PREFIX_DIR_PATH + "/");
|
||||
path = path.replaceAll("^~/$", TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
path = path.replaceAll("^~/", TermuxConstants.TERMUX_HOME_DIR_PATH + "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
||||
*
|
||||
* @param paths The {@code paths} to unexpand.
|
||||
* @return Returns the {@code unexpand paths}.
|
||||
*/
|
||||
public static List<String> getUnExpandedTermuxPaths(List<String> paths) {
|
||||
if (paths == null) return null;
|
||||
List<String> unExpandedPaths = new ArrayList<>();
|
||||
|
||||
for (int i = 0; i < paths.size(); i++) {
|
||||
unExpandedPaths.add(getUnExpandedTermuxPath(paths.get(i)));
|
||||
}
|
||||
|
||||
return unExpandedPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace termux absolute paths with "$PREFIX/" or "~/" prefix.
|
||||
*
|
||||
* @param path The {@code path} to unexpand.
|
||||
* @return Returns the {@code unexpand path}.
|
||||
*/
|
||||
public static String getUnExpandedTermuxPath(String path) {
|
||||
if (path != null && !path.isEmpty()) {
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_PREFIX_DIR_PATH) + "/", "\\$PREFIX/");
|
||||
path = path.replaceAll("^" + Pattern.quote(TermuxConstants.TERMUX_HOME_DIR_PATH) + "/", "~/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get canonical path.
|
||||
*
|
||||
* @param path The {@code path} to convert.
|
||||
* @param prefixForNonAbsolutePath Optional prefix path to prefix before non-absolute paths. This
|
||||
* can be set to {@code null} if non-absolute paths should
|
||||
* be prefixed with "/". The call to {@link File#getCanonicalPath()}
|
||||
* will automatically do this anyways.
|
||||
* @param expandPath The {@code boolean} that decides if input path is first attempted to be expanded by calling
|
||||
* {@link TermuxFileUtils#getExpandedTermuxPath(String)} before its passed to
|
||||
* {@link FileUtils#getCanonicalPath(String, String)}.
|
||||
|
||||
* @return Returns the {@code canonical path}.
|
||||
*/
|
||||
public static String getCanonicalPath(String path, final String prefixForNonAbsolutePath, final boolean expandPath) {
|
||||
if (path == null) path = "";
|
||||
|
||||
if (expandPath)
|
||||
path = getExpandedTermuxPath(path);
|
||||
|
||||
return FileUtils.getCanonicalPath(path, prefixForNonAbsolutePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@code path} is under the allowed termux working directory paths. If it is, then
|
||||
* allowed parent path is returned.
|
||||
*
|
||||
* @param path The {@code path} to check.
|
||||
* @return Returns the allowed path if it {@code path} is under it, otherwise {@link TermuxConstants#TERMUX_FILES_DIR_PATH}.
|
||||
*/
|
||||
public static String getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String path) {
|
||||
if (path == null || path.isEmpty()) return TermuxConstants.TERMUX_FILES_DIR_PATH;
|
||||
|
||||
if (path.startsWith(TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH + "/")) {
|
||||
return TermuxConstants.TERMUX_STORAGE_HOME_DIR_PATH;
|
||||
} if (path.startsWith(Environment.getExternalStorageDirectory().getAbsolutePath() + "/")) {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
} else if (path.startsWith("/sdcard/")) {
|
||||
return "/sdcard";
|
||||
} else {
|
||||
return TermuxConstants.TERMUX_FILES_DIR_PATH;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the existence and permissions of directory file at path as a working directory for
|
||||
* termux app.
|
||||
*
|
||||
* The creation of missing directory and setting of missing permissions will only be done if
|
||||
* {@code path} is under paths returned by {@link #getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(String)}.
|
||||
*
|
||||
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
|
||||
*
|
||||
* @param label The optional label for the directory file. This can optionally be {@code null}.
|
||||
* @param filePath The {@code path} for file to validate or create. Symlinks will not be followed.
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||
* should be created if its missing.
|
||||
* @param setPermissions The {@code boolean} that decides if permissions are to be
|
||||
* automatically set defined by {@code permissionsToCheck}.
|
||||
* @param setMissingPermissionsOnly The {@code boolean} that decides if only missing permissions
|
||||
* are to be set or if they should be overridden.
|
||||
* @param ignoreErrorsIfPathIsInParentDirPath The {@code boolean} that decides if existence
|
||||
* and permission errors are to be ignored if path is
|
||||
* in {@code parentDirPath}.
|
||||
* @param ignoreIfNotExecutable The {@code boolean} that decides if missing executable permission
|
||||
* error is to be ignored. This allows making an attempt to set
|
||||
* executable permissions, but ignoring if it fails.
|
||||
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||
* or validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static Error validateDirectoryFileExistenceAndPermissions(String label, final String filePath, final boolean createDirectoryIfMissing,
|
||||
final boolean setPermissions, final boolean setMissingPermissionsOnly,
|
||||
final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) {
|
||||
return FileUtils.validateDirectoryFileExistenceAndPermissions(label, filePath,
|
||||
TermuxFileUtils.getMatchedAllowedTermuxWorkingDirectoryParentPathForPath(filePath), createDirectoryIfMissing,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setPermissions, setMissingPermissionsOnly,
|
||||
ignoreErrorsIfPathIsInParentDirPath, ignoreIfNotExecutable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if {@link TermuxConstants#TERMUX_FILES_DIR_PATH} exists and has
|
||||
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
|
||||
*
|
||||
* This is required because binaries compiled for termux are hard coded with
|
||||
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and the path must be accessible.
|
||||
*
|
||||
* The permissions set to directory will be {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS}.
|
||||
*
|
||||
* This function does not create the directory manually but by calling {@link Context#getFilesDir()}
|
||||
* so that android itself creates it. However, the call will not create its parent package
|
||||
* data directory `/data/user/0/[package_name]` if it does not already exist and a `logcat`
|
||||
* error will be logged by android.
|
||||
* {@code Failed to ensure /data/user/0/<package_name>/files: mkdir failed: ENOENT (No such file or directory)}
|
||||
* An android app normally can't create the package data directory since its parent `/data/user/0`
|
||||
* is owned by `system` user and is normally created at app install or update time and not at app startup.
|
||||
*
|
||||
* Note that the path returned by {@link Context#getFilesDir()} may
|
||||
* be under `/data/user/[id]/[package_name]` instead of `/data/data/[package_name]`
|
||||
* defined by default by {@link TermuxConstants#TERMUX_FILES_DIR_PATH} where id will be 0 for
|
||||
* primary user and a higher number for other users/profiles. If app is running under work profile
|
||||
* or secondary user, then {@link TermuxConstants#TERMUX_FILES_DIR_PATH} will not be accessible
|
||||
* and will not be automatically created, unless there is a bind mount from `/data/data` to
|
||||
* `/data/user/[id]`, ideally in the right namespace.
|
||||
* https://source.android.com/devices/tech/admin/multi-user
|
||||
*
|
||||
*
|
||||
* On Android version `<=10`, the `/data/user/0` is a symlink to `/data/data` directory.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-10.0.0_r47:system/core/rootdir/init.rc;l=589
|
||||
* {@code
|
||||
* symlink /data/data /data/user/0
|
||||
* }
|
||||
*
|
||||
* {@code
|
||||
* /system/bin/ls -lhd /data/data /data/user/0
|
||||
* drwxrwx--x 179 system system 8.0K 2021-xx-xx xx:xx /data/data
|
||||
* lrwxrwxrwx 1 root root 10 2021-xx-xx xx:xx /data/user/0 -> /data/data
|
||||
* }
|
||||
*
|
||||
* On Android version `>=11`, the `/data/data` directory is bind mounted at `/data/user/0`.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r40:system/core/rootdir/init.rc;l=705
|
||||
* https://cs.android.com/android/_/android/platform/system/core/+/3cca270e95ca8d8bc8b800e2b5d7da1825fd7100
|
||||
* {@code
|
||||
* # Unlink /data/user/0 if we previously symlink it to /data/data
|
||||
* rm /data/user/0
|
||||
*
|
||||
* # Bind mount /data/user/0 to /data/data
|
||||
* mkdir /data/user/0 0700 system system encryption=None
|
||||
* mount none /data/data /data/user/0 bind rec
|
||||
* }
|
||||
*
|
||||
* {@code
|
||||
* /system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1
|
||||
* 87 32 253:5 / /data rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
|
||||
* 91 87 253:5 /data /data/user/0 rw,nosuid,nodev,noatime shared:27 - ext4 /dev/block/dm-5 rw,seclabel,resgid=1065,errors=panic
|
||||
* }
|
||||
*
|
||||
* The column 4 defines the root of the mount within the filesystem.
|
||||
* Basically, `/dev/block/dm-5/` is mounted at `/data` and `/dev/block/dm-5/data` is mounted at
|
||||
* `/data/user/0`.
|
||||
* https://www.kernel.org/doc/Documentation/filesystems/proc.txt (section 3.5)
|
||||
* https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt
|
||||
* https://unix.stackexchange.com/a/571959
|
||||
*
|
||||
*
|
||||
* Also note that running `/system/bin/ls -lhd /data/user/0/com.termux` as secondary user will result
|
||||
* in `ls: /data/user/0/com.termux: Permission denied` where `0` is primary user id but running
|
||||
* `/system/bin/ls -lhd /data/user/10/com.termux` will result in
|
||||
* `drwx------ 6 u10_a149 u10_a149 4.0K 2021-xx-xx xx:xx /data/user/10/com.termux` where `10` is
|
||||
* secondary user id. So can't stat directory (not contents) of primary user from secondary user
|
||||
* but can the other way around. However, this is happening on android 10 avd, but not on android
|
||||
* 11 avd.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||
* should be created if its missing.
|
||||
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
|
||||
* automatically set.
|
||||
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||
* or validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static Error isTermuxFilesDirectoryAccessible(@NonNull final Context context, boolean createDirectoryIfMissing, boolean setMissingPermissions) {
|
||||
if (createDirectoryIfMissing)
|
||||
context.getFilesDir();
|
||||
|
||||
if (!FileUtils.directoryFileExists(TermuxConstants.TERMUX_FILES_DIR_PATH, true))
|
||||
return FileUtilsErrno.ERRNO_FILE_NOT_FOUND_AT_PATH.getError("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH);
|
||||
|
||||
if (setMissingPermissions)
|
||||
FileUtils.setMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS);
|
||||
|
||||
return FileUtils.checkMissingFilePermissions("termux files directory", TermuxConstants.TERMUX_FILES_DIR_PATH,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} exists and has
|
||||
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
|
||||
* .
|
||||
*
|
||||
* The {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} directory would not exist if termux has
|
||||
* not been installed or the bootstrap setup has not been run or if it was deleted by the user.
|
||||
*
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||
* should be created if its missing.
|
||||
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
|
||||
* automatically set.
|
||||
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||
* or validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static Error isTermuxPrefixDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
|
||||
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix directory", TermuxConstants.TERMUX_PREFIX_DIR_PATH,
|
||||
null, createDirectoryIfMissing,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
|
||||
false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if {@link TermuxConstants#TERMUX_STAGING_PREFIX_DIR_PATH} exists and has
|
||||
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions.
|
||||
*
|
||||
* @param createDirectoryIfMissing The {@code boolean} that decides if directory file
|
||||
* should be created if its missing.
|
||||
* @param setMissingPermissions The {@code boolean} that decides if permissions are to be
|
||||
* automatically set.
|
||||
* @return Returns the {@code error} if path is not a directory file, failed to create it,
|
||||
* or validating permissions failed, otherwise {@code null}.
|
||||
*/
|
||||
public static Error isTermuxPrefixStagingDirectoryAccessible(boolean createDirectoryIfMissing, boolean setMissingPermissions) {
|
||||
return FileUtils.validateDirectoryFileExistenceAndPermissions("termux prefix staging directory", TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH,
|
||||
null, createDirectoryIfMissing,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, setMissingPermissions, true,
|
||||
false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for stat output for various Termux app files paths.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getTermuxFilesStatMarkdownString(@NonNull final Context context) {
|
||||
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(context);
|
||||
if (termuxPackageContext == null) return null;
|
||||
|
||||
// Also ensures that termux files directory is created if it does not already exist
|
||||
String filesDir = termuxPackageContext.getFilesDir().getAbsolutePath();
|
||||
|
||||
// Build script
|
||||
StringBuilder statScript = new StringBuilder();
|
||||
statScript
|
||||
.append("echo 'ls info:'\n")
|
||||
.append("/system/bin/ls -lhdZ")
|
||||
.append(" '/data/data'")
|
||||
.append(" '/data/user/0'")
|
||||
.append(" '" + TermuxConstants.TERMUX_INTERNAL_PRIVATE_APP_DATA_DIR_PATH + "'")
|
||||
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "'")
|
||||
.append(" '" + TermuxConstants.TERMUX_FILES_DIR_PATH + "'")
|
||||
.append(" '" + filesDir + "'")
|
||||
.append(" '/data/user/0/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
|
||||
.append(" '/data/user/" + TermuxConstants.TERMUX_PACKAGE_NAME + "/files'")
|
||||
.append(" '" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "'")
|
||||
.append(" '" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "'")
|
||||
.append(" '" + TermuxConstants.TERMUX_HOME_DIR_PATH + "'")
|
||||
.append(" '" + TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/login'")
|
||||
.append(" 2>&1")
|
||||
.append("\necho; echo 'mount info:'\n")
|
||||
.append("/system/bin/grep -E '( /data )|( /data/data )|( /data/user/[0-9]+ )' /proc/self/mountinfo 2>&1 | /system/bin/grep -v '/data_mirror' 2>&1");
|
||||
|
||||
// Run script
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, statScript.toString() + "\n", "/", true, true);
|
||||
executionCommand.commandLabel = TermuxConstants.TERMUX_APP_NAME + " Files Stat Command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
|
||||
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||
if (termuxTask == null || !executionCommand.isSuccessful()) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build script output
|
||||
StringBuilder statOutput = new StringBuilder();
|
||||
statOutput.append("$ ").append(statScript.toString());
|
||||
statOutput.append("\n\n").append(executionCommand.resultData.stdout.toString());
|
||||
|
||||
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
||||
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
if (stderrSet)
|
||||
statOutput.append("\n").append(executionCommand.resultData.stderr.toString());
|
||||
statOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
|
||||
}
|
||||
|
||||
// Build markdown output
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" Files Info\n\n");
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"TERMUX_REQUIRED_FILES_DIR_PATH ($PREFIX)", TermuxConstants.TERMUX_FILES_DIR_PATH);
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"ANDROID_ASSIGNED_FILES_DIR_PATH", filesDir);
|
||||
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(statOutput.toString(), true));
|
||||
markdownString.append("\n##\n");
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
@ -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<FilePermission> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() != null && !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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() + "\"");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,310 @@
|
|||
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 com.termux.shared.models.errors.Error;
|
||||
|
||||
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);
|
||||
assertEqual("FileUtilsTests directory path is not a canonical path", testRootDirectoryPath, fileUtilsTestsDirectoryCanonicalPath);
|
||||
|
||||
runTestsInner(testRootDirectoryPath);
|
||||
Logger.logInfo(LOG_TAG, "All tests successful");
|
||||
} catch (Exception e) {
|
||||
Logger.logErrorExtended(LOG_TAG, e.getMessage());
|
||||
Logger.showToast(context, e.getMessage() != null ? e.getMessage().replaceAll("(?s)\nFull Error:\n.*", "") : null, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void runTestsInner(@NonNull final String testRootDirectoryPath) throws Exception {
|
||||
Error error;
|
||||
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";
|
||||
error = FileUtils.clearDirectory(label, testRootDirectoryPath);
|
||||
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||
|
||||
if (!FileUtils.directoryFileExists(testRootDirectoryPath, false))
|
||||
throwException("The " + label + " directory file does not exist as expected after creation");
|
||||
|
||||
|
||||
// Create dir1 directory file
|
||||
error = FileUtils.createDirectoryFile(dir1_label, dir1_path);
|
||||
assertEqual("Failed to create " + dir1_label + " directory file", null, error);
|
||||
|
||||
// Create dir2 directory file
|
||||
error = FileUtils.createDirectoryFile(dir2_label, dir2_path);
|
||||
assertEqual("Failed to create " + dir2_label + " directory file", null, error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Create dir1/sub_dir1 directory file
|
||||
label = dir1__sub_dir1_label; path = dir1__sub_dir1_path;
|
||||
error = FileUtils.createDirectoryFile(label, path);
|
||||
assertEqual("Failed to create " + label + " directory file", null, error);
|
||||
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;
|
||||
error = FileUtils.createRegularFile(label, path);
|
||||
assertEqual("Failed to create " + label + " regular file", null, error);
|
||||
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;
|
||||
error = FileUtils.createSymlinkFile(label, dir2_path, path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
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;
|
||||
error = FileUtils.copySymlinkFile(label, dir1__sub_sym1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir1__sub_sym1_label + " symlink file to " + label, null, error);
|
||||
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;
|
||||
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "line1", false);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode false", null, error);
|
||||
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
|
||||
error = FileUtils.writeStringToFile(label, path, Charset.defaultCharset(), "\nline2", true);
|
||||
assertEqual("Failed to write string to " + label + " file with append mode true", null, error);
|
||||
|
||||
// Read dir2/sub_reg1 regular file
|
||||
StringBuilder dataStringBuilder = new StringBuilder();
|
||||
error = FileUtils.readStringFromFile(label, path, Charset.defaultCharset(), dataStringBuilder, false);
|
||||
assertEqual("Failed to read from " + label + " file", null, error);
|
||||
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;
|
||||
error = FileUtils.copyRegularFile(label, dir2__sub_reg1_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2__sub_reg1_label + " regular file to " + label, null, error);
|
||||
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;
|
||||
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||
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;
|
||||
error = FileUtils.copyDirectoryFile(label, dir2_path, path, false);
|
||||
assertEqual("Failed to copy " + dir2_label + " directory file to " + label, null, error);
|
||||
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;
|
||||
error = FileUtils.moveDirectoryFile(label, dir3_path, path, false);
|
||||
assertEqual("Failed to move " + dir3_label + " directory file to " + label, null, error);
|
||||
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;
|
||||
error = FileUtils.createSymlinkFile(label, "../dir4", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
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;
|
||||
error = FileUtils.createSymlinkFile(label, "../dirX", path);
|
||||
assertEqual("Failed to create " + label + " symlink file", null, error);
|
||||
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;
|
||||
error = FileUtils.deleteSymlinkFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " symlink file", null, error);
|
||||
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;
|
||||
error = FileUtils.deleteDirectoryFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " directory file", null, error);
|
||||
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;
|
||||
error = FileUtils.deleteRegularFile(label, path, false);
|
||||
assertEqual("Failed to delete " + label + " regular file", null, error);
|
||||
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 Error actual) throws Exception {
|
||||
String actualString = actual != null ? actual.getMessage() : null;
|
||||
if (!equalsRegardingNull(expected, actualString))
|
||||
throwException(message + "\nexpected: \"" + expected + "\"\nactual: \"" + actualString + "\"\nFull Error:\n" + (actual != null ? actual.toString() : ""));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
package com.termux.shared.interact;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class MessageDialogUtils {
|
||||
|
||||
/**
|
||||
* Show a message in a dialog
|
||||
*
|
||||
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
|
||||
* must be passed, otherwise exceptions will be thrown.
|
||||
* @param titleText The title text of the dialog.
|
||||
* @param messageText The message text of the dialog.
|
||||
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
|
||||
*/
|
||||
public static void showMessage(Context context, String titleText, String messageText, final DialogInterface.OnDismissListener onDismiss) {
|
||||
showMessage(context, titleText, messageText, null, null, null, null, onDismiss);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a message in a dialog
|
||||
*
|
||||
* @param context The {@link Context} to use to start the dialog. An {@link Activity} {@link Context}
|
||||
* must be passed, otherwise exceptions will be thrown.
|
||||
* @param titleText The title text of the dialog.
|
||||
* @param messageText The message text of the dialog.
|
||||
* @param positiveText The positive button text of the dialog.
|
||||
* @param onPositiveButton The {@link DialogInterface.OnClickListener} to run when positive button
|
||||
* is pressed.
|
||||
* @param negativeText The negative button text of the dialog. If this is {@code null}, then
|
||||
* negative button will not be shown.
|
||||
* @param onNegativeButton The {@link DialogInterface.OnClickListener} to run when negative button
|
||||
* is pressed.
|
||||
* @param onDismiss The {@link DialogInterface.OnDismissListener} to run when dialog is dismissed.
|
||||
*/
|
||||
public static void showMessage(Context context, String titleText, String messageText,
|
||||
String positiveText,
|
||||
final DialogInterface.OnClickListener onPositiveButton,
|
||||
String negativeText,
|
||||
final DialogInterface.OnClickListener onNegativeButton,
|
||||
final DialogInterface.OnDismissListener onDismiss) {
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Theme_AppCompat_Light_Dialog);
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );
|
||||
View view = inflater.inflate(R.layout.dialog_show_message, null);
|
||||
if (view != null) {
|
||||
builder.setView(view);
|
||||
|
||||
TextView titleView = view.findViewById(R.id.dialog_title);
|
||||
if (titleView != null)
|
||||
titleView.setText(titleText);
|
||||
|
||||
TextView messageView = view.findViewById(R.id.dialog_message);
|
||||
if (messageView != null)
|
||||
messageView.setText(messageText);
|
||||
}
|
||||
|
||||
if (positiveText == null)
|
||||
positiveText = context.getString(android.R.string.ok);
|
||||
builder.setPositiveButton(positiveText, onPositiveButton);
|
||||
|
||||
if (negativeText != null)
|
||||
builder.setNegativeButton(negativeText, onNegativeButton);
|
||||
|
||||
if (onDismiss != null)
|
||||
builder.setOnDismissListener(onDismiss);
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
dialog.setOnShowListener(dialogInterface -> {
|
||||
Logger.logError("dialog");
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
if (button != null)
|
||||
button.setTextColor(Color.BLACK);
|
||||
button = dialog.getButton(AlertDialog.BUTTON_NEGATIVE);
|
||||
if (button != null)
|
||||
button.setTextColor(Color.BLACK);
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public static void exitAppWithErrorMessage(Context context, String titleText, String messageText) {
|
||||
showMessage(context, titleText, messageText, dialog -> System.exit(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
package com.termux.shared.interact;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.packages.PermissionUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ShareUtils {
|
||||
|
||||
private static final String LOG_TAG = "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);
|
||||
try {
|
||||
context.startActivity(chooserIntent);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open system chooser for:\n" + IntentUtils.getIntentString(chooserIntent), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
shareText(context, subject, text, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Share text.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param subject The subject for sharing.
|
||||
* @param text The text to share.
|
||||
* @param title The title for share menu.
|
||||
*/
|
||||
public static void shareText(final Context context, final String subject, final String text, @Nullable final String title) {
|
||||
if (context == null || text == 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, true, false, false));
|
||||
|
||||
openSystemAppChooser(context, shareTextIntent, DataUtils.isNullOrEmpty(title) ? context.getString(R.string.title_share_with) : title);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel` and `toastString`. */
|
||||
public static void copyTextToClipboard(Context context, final String text) {
|
||||
copyTextToClipboard(context, null, text, null);
|
||||
}
|
||||
|
||||
/** Wrapper for {@link #copyTextToClipboard(Context, String, String, String)} with `null` `clipDataLabel`. */
|
||||
public static void copyTextToClipboard(Context context, final String text, final String toastString) {
|
||||
copyTextToClipboard(context, null, text, toastString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the text to primary clip of the clipboard.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param clipDataLabel The label to show to the user describing the copied text.
|
||||
* @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(Context context, @Nullable final String clipDataLabel,
|
||||
final String text, final String toastString) {
|
||||
if (context == null || text == null) return;
|
||||
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboardManager == null) return;
|
||||
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText(clipDataLabel,
|
||||
DataUtils.getTruncatedCommandOutput(text, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES,
|
||||
true, false, false)));
|
||||
|
||||
if (toastString != null && !toastString.isEmpty())
|
||||
Logger.showToast(context, toastString, true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getTextFromClipboard(Context, boolean)} that returns primary text {@link String}
|
||||
* if its set and not empty.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getTextStringFromClipboardIfSet(Context context, boolean coerceToText) {
|
||||
CharSequence textCharSequence = getTextFromClipboard(context, coerceToText);
|
||||
if (textCharSequence == null) return null;
|
||||
String textString = textCharSequence.toString();
|
||||
return !textString.isEmpty() ? textString : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the text from primary clip of the clipboard.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param coerceToText Whether to call {@link ClipData.Item#coerceToText(Context)} to coerce
|
||||
* non-text data to text.
|
||||
* @return Returns the {@link CharSequence} of primary text. This will be `null` if failed to get it.
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence getTextFromClipboard(Context context, boolean coerceToText) {
|
||||
if (context == null) return null;
|
||||
|
||||
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
if (clipboardManager == null) return null;
|
||||
|
||||
ClipData clipData = clipboardManager.getPrimaryClip();
|
||||
if (clipData == null) return null;
|
||||
|
||||
ClipData.Item clipItem = clipData.getItemAt(0);
|
||||
if (clipItem == null) return null;
|
||||
|
||||
return coerceToText ? clipItem.coerceToText(context) : clipItem.getText();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Open a url.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param url The url to open.
|
||||
*/
|
||||
public static void openURL(final Context context, final String url) {
|
||||
if (context == null || url == null || url.isEmpty()) return;
|
||||
Uri uri = Uri.parse(url);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
// If no activity found to handle intent, show system chooser
|
||||
openSystemAppChooser(context, intent, context.getString(R.string.title_open_url_with));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to open url \"" + url + "\"", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a file at the path.
|
||||
*
|
||||
* If if path is under {@link Environment#getExternalStorageDirectory()}
|
||||
* or `/sdcard` and storage permission is missing, it will be requested if {@code context} is an
|
||||
* instance of {@link Activity} or {@link AppCompatActivity} and {@code storagePermissionRequestCode}
|
||||
* is `>=0` and the function will automatically return. The caller should call this function again
|
||||
* if user granted the permission.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param label The label for file.
|
||||
* @param filePath The path to save the file.
|
||||
* @param text The text to write to file.
|
||||
* @param showToast If set to {@code true}, then a toast is shown if saving to file is successful.
|
||||
* @param storagePermissionRequestCode The request code to use while asking for permission.
|
||||
*/
|
||||
public static void saveTextToFile(final Context context, final String label, final String filePath, final String text, final boolean showToast, final int storagePermissionRequestCode) {
|
||||
if (context == null || filePath == null || filePath.isEmpty() || text == null) return;
|
||||
|
||||
// If path is under primary external storage directory, then check for missing permissions.
|
||||
if ((FileUtils.isPathInDirPath(filePath, Environment.getExternalStorageDirectory().getAbsolutePath(), true) ||
|
||||
FileUtils.isPathInDirPath(filePath, "/sdcard", true)) &&
|
||||
!PermissionUtils.checkPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
Logger.logErrorAndShowToast(context, LOG_TAG, context.getString(R.string.msg_storage_permission_not_granted));
|
||||
|
||||
if (storagePermissionRequestCode >= 0) {
|
||||
if (context instanceof AppCompatActivity)
|
||||
PermissionUtils.requestPermission(((AppCompatActivity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
|
||||
else if (context instanceof Activity)
|
||||
PermissionUtils.requestPermission(((Activity) context), Manifest.permission.WRITE_EXTERNAL_STORAGE, storagePermissionRequestCode);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Error error = FileUtils.writeStringToFile(label, filePath,
|
||||
Charset.defaultCharset(), text, false);
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(LOG_TAG, error.toString());
|
||||
Logger.showToast(context, Error.getMinimalErrorString(error), true);
|
||||
} else {
|
||||
if (showToast)
|
||||
Logger.showToast(context, context.getString(R.string.msg_file_saved_successfully, label, filePath), true);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package com.termux.shared.interact;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.Selection;
|
||||
import android.util.TypedValue;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.termux.shared.R;
|
||||
|
||||
public final class TextInputDialogUtils {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
448
termux-shared/src/main/java/com/termux/shared/logger/Logger.java
Normal file
448
termux-shared/src/main/java/com/termux/shared/logger/Logger.java
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
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.data.DataUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.ArrayList;
|
||||
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;
|
||||
public static final int MAX_LOG_LEVEL = LOG_LEVEL_VERBOSE;
|
||||
private static int CURRENT_LOG_LEVEL = DEFAULT_LOG_LEVEL;
|
||||
|
||||
/**
|
||||
* The maximum size of the log entry payload that can be written to the logger. An attempt to
|
||||
* write more than this amount will result in a truncated log entry.
|
||||
*
|
||||
* The limit is 4068 but this includes log tag and log level prefix "D/" before log tag and ": "
|
||||
* suffix after it.
|
||||
*
|
||||
* #define LOGGER_ENTRY_MAX_PAYLOAD 4068
|
||||
* https://cs.android.com/android/_/android/platform/system/core/+/android10-release:liblog/include/log/log_read.h;l=127
|
||||
*/
|
||||
public static final int LOGGER_ENTRY_MAX_PAYLOAD = 4068; // 4068 bytes
|
||||
|
||||
/**
|
||||
* The maximum safe size of the log entry payload that can be written to the logger, based on
|
||||
* {@link #LOGGER_ENTRY_MAX_PAYLOAD}. Using 4000 as a safe limit to give log tag and its
|
||||
* prefix/suffix max 68 characters for itself. Use "log*Extended()" functions to use max possible
|
||||
* limit if tag is already known.
|
||||
*/
|
||||
public static final int LOGGER_ENTRY_MAX_SAFE_PAYLOAD = 4000; // 4000 bytes
|
||||
|
||||
|
||||
|
||||
public static void logMessage(int logPriority, String tag, String message) {
|
||||
if (logPriority == Log.ERROR && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
||||
Log.e(getFullTag(tag), message);
|
||||
else if (logPriority == Log.WARN && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
||||
Log.w(getFullTag(tag), message);
|
||||
else if (logPriority == Log.INFO && CURRENT_LOG_LEVEL >= LOG_LEVEL_NORMAL)
|
||||
Log.i(getFullTag(tag), message);
|
||||
else if (logPriority == Log.DEBUG && CURRENT_LOG_LEVEL >= LOG_LEVEL_DEBUG)
|
||||
Log.d(getFullTag(tag), message);
|
||||
else if (logPriority == Log.VERBOSE && CURRENT_LOG_LEVEL >= LOG_LEVEL_VERBOSE)
|
||||
Log.v(getFullTag(tag), message);
|
||||
}
|
||||
|
||||
public static void logExtendedMessage(int logLevel, String tag, String message) {
|
||||
if (message == null) return;
|
||||
|
||||
int cutOffIndex;
|
||||
int nextNewlineIndex;
|
||||
String prefix = "";
|
||||
|
||||
// -8 for prefix "(xx/xx)" (max 99 sections), - log tag length, -4 for log tag prefix "D/" and suffix ": "
|
||||
int maxEntrySize = LOGGER_ENTRY_MAX_PAYLOAD - 8 - getFullTag(tag).length() - 4;
|
||||
|
||||
List<String> messagesList = new ArrayList<>();
|
||||
|
||||
while(!message.isEmpty()) {
|
||||
if (message.length() > maxEntrySize) {
|
||||
cutOffIndex = maxEntrySize;
|
||||
nextNewlineIndex = message.lastIndexOf('\n', cutOffIndex);
|
||||
if (nextNewlineIndex != -1) {
|
||||
cutOffIndex = nextNewlineIndex + 1;
|
||||
}
|
||||
messagesList.add(message.substring(0, cutOffIndex));
|
||||
message = message.substring(cutOffIndex);
|
||||
} else {
|
||||
messagesList.add(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i=0; i<messagesList.size(); i++) {
|
||||
if (messagesList.size() > 1)
|
||||
prefix = "(" + (i + 1) + "/" + messagesList.size() + ")\n";
|
||||
logMessage(logLevel, tag, prefix + messagesList.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 logErrorExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.ERROR, tag, message);
|
||||
}
|
||||
|
||||
public static void logErrorExtended(String message) {
|
||||
logExtendedMessage(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 logWarnExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.WARN, tag, message);
|
||||
}
|
||||
|
||||
public static void logWarnExtended(String message) {
|
||||
logExtendedMessage(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 logInfoExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.INFO, tag, message);
|
||||
}
|
||||
|
||||
public static void logInfoExtended(String message) {
|
||||
logExtendedMessage(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 logDebugExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.DEBUG, tag, message);
|
||||
}
|
||||
|
||||
public static void logDebugExtended(String message) {
|
||||
logExtendedMessage(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 logVerboseExtended(String tag, String message) {
|
||||
logExtendedMessage(Log.VERBOSE, tag, message);
|
||||
}
|
||||
|
||||
public static void logVerboseExtended(String message) {
|
||||
logExtendedMessage(Log.VERBOSE, DEFAULT_LOG_TAG, message);
|
||||
}
|
||||
|
||||
public static void logVerboseForce(String tag, String message) {
|
||||
Log.v(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) {
|
||||
Logger.logErrorExtended(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> throwablesList) {
|
||||
Logger.logErrorExtended(tag, getMessageAndStackTracesString(message, throwablesList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
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> throwablesList) {
|
||||
if (message == null && (throwablesList == null || throwablesList.size() == 0))
|
||||
return null;
|
||||
else if (message != null && (throwablesList != null && throwablesList.size() != 0))
|
||||
return message + ":\n" + getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||
else if (throwablesList == null || throwablesList.size() == 0)
|
||||
return message;
|
||||
else
|
||||
return getStackTracesString(null, getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return stackTraceString;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String[] getStackTracesStringArray(Throwable throwable) {
|
||||
return getStackTracesStringArray(Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public static String[] getStackTracesStringArray(List<Throwable> throwablesList) {
|
||||
if (throwablesList == null) return null;
|
||||
final String[] stackTraceStringArray = new String[throwablesList.size()];
|
||||
for (int i = 0; i < throwablesList.size(); i++) {
|
||||
stackTraceStringArray[i] = getStackTraceString(throwablesList.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 || DataUtils.isNullOrEmpty(toastText)) 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 (isLogLevelValid(logLevel))
|
||||
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;
|
||||
}
|
||||
|
||||
public static boolean isLogLevelValid(Integer logLevel) {
|
||||
return (logLevel != null && logLevel >= LOG_LEVEL_OFF && logLevel <= MAX_LOG_LEVEL);
|
||||
}
|
||||
|
||||
/** Check if custom log level is valid and >= {@link #CURRENT_LOG_LEVEL}. If custom log level is
|
||||
* not valid then {@link #LOG_LEVEL_VERBOSE} must be >= {@link #CURRENT_LOG_LEVEL}. */
|
||||
public static boolean shouldEnableLoggingForCustomLogLevel(Integer customLogLevel) {
|
||||
if (customLogLevel == null || CURRENT_LOG_LEVEL <= LOG_LEVEL_OFF || customLogLevel <= LOG_LEVEL_OFF) return false;
|
||||
customLogLevel = Logger.isLogLevelValid(customLogLevel) ? customLogLevel: Logger.LOG_LEVEL_VERBOSE;
|
||||
return (customLogLevel >= CURRENT_LOG_LEVEL);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
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;
|
||||
String match;
|
||||
|
||||
Matcher matcher = backticksPattern.matcher(string);
|
||||
while(matcher.find()) {
|
||||
match = matcher.group(1);
|
||||
matchCount = match != null ? match.length() : 0;
|
||||
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, String url) {
|
||||
if (url != null)
|
||||
return "[" + label.replaceAll("]", "\\\\]") + "](" + url.replaceAll("\\)", "\\\\)") + ")";
|
||||
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(48)
|
||||
})
|
||||
// NB! both ordered and bullet list items
|
||||
.setFactory(ListItem.class, (configuration, props) -> new BulletSpan());
|
||||
}
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Spanned getSpannedMarkdownText(Context context, String string) {
|
||||
if (context == null || string == null) return null;
|
||||
final Markwon markwon = getSpannedMarkwonBuilder(context);
|
||||
return markwon.toMarkdown(string);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,552 @@
|
|||
package com.termux.shared.models;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.data.IntentUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ExecutionCommand {
|
||||
|
||||
/*
|
||||
The {@link ExecutionState#SUCCESS} and {@link ExecutionState#FAILED} is defined based on
|
||||
successful execution of command without any internal errors or exceptions being raised.
|
||||
The shell command {@link #exitCode} being non-zero **does not** mean that execution command failed.
|
||||
Only the {@link #errCode} being non-zero means that execution command failed from the Termux app
|
||||
perspective.
|
||||
*/
|
||||
|
||||
/** The {@link Enum} that defines {@link ExecutionCommand} state. */
|
||||
public enum ExecutionState {
|
||||
|
||||
PRE_EXECUTION("Pre-Execution", 0),
|
||||
EXECUTING("Executing", 1),
|
||||
EXECUTED("Executed", 2),
|
||||
SUCCESS("Success", 3),
|
||||
FAILED("Failed", 4);
|
||||
|
||||
private final String name;
|
||||
private final int value;
|
||||
|
||||
ExecutionState(final String name, final int value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** The optional unique id for the {@link ExecutionCommand}. */
|
||||
public Integer id;
|
||||
|
||||
|
||||
/** The current state of the {@link ExecutionCommand}. */
|
||||
private ExecutionState currentState = ExecutionState.PRE_EXECUTION;
|
||||
/** The previous state of the {@link ExecutionCommand}. */
|
||||
private ExecutionState previousState = ExecutionState.PRE_EXECUTION;
|
||||
|
||||
|
||||
/** The executable for the {@link ExecutionCommand}. */
|
||||
public String executable;
|
||||
/** The executable Uri for the {@link ExecutionCommand}. */
|
||||
public Uri executableUri;
|
||||
/** The executable arguments array for the {@link ExecutionCommand}. */
|
||||
public String[] arguments;
|
||||
/** The stdin string for the {@link ExecutionCommand}. */
|
||||
public String stdin;
|
||||
/** The current working directory for the {@link ExecutionCommand}. */
|
||||
public String workingDirectory;
|
||||
|
||||
|
||||
/** The terminal transcript rows for the {@link ExecutionCommand}. */
|
||||
public Integer terminalTranscriptRows;
|
||||
|
||||
|
||||
/** If the {@link ExecutionCommand} is a background or a foreground terminal session command. */
|
||||
public boolean inBackground;
|
||||
/** If the {@link ExecutionCommand} is meant to start a failsafe terminal session. */
|
||||
public boolean isFailsafe;
|
||||
|
||||
/**
|
||||
* The {@link ExecutionCommand} custom log level for background {@link com.termux.shared.shell.TermuxTask}
|
||||
* commands. By default, @link com.termux.shared.shell.StreamGobbler} only logs stdout and
|
||||
* stderr if {@link Logger} `CURRENT_LOG_LEVEL` is >= {@link Logger#LOG_LEVEL_VERBOSE} and
|
||||
* {@link com.termux.shared.shell.TermuxTask} only logs stdin if `CURRENT_LOG_LEVEL` is >=
|
||||
* {@link Logger#LOG_LEVEL_DEBUG}.
|
||||
*/
|
||||
public Integer backgroundCustomLogLevel;
|
||||
|
||||
/** The session action of foreground commands. */
|
||||
public String sessionAction;
|
||||
|
||||
|
||||
/** The command label for the {@link ExecutionCommand}. */
|
||||
public String commandLabel;
|
||||
/** The markdown text for the command description for the {@link ExecutionCommand}. */
|
||||
public String commandDescription;
|
||||
/** The markdown text for the help of command for the {@link ExecutionCommand}. This can be used
|
||||
* to provide useful info to the user if an internal error is raised. */
|
||||
public String commandHelp;
|
||||
|
||||
|
||||
/** Defines the markdown text for the help of the Termux plugin API that was used to start the
|
||||
* {@link ExecutionCommand}. This can be used to provide useful info to the user if an internal
|
||||
* error is raised. */
|
||||
public String pluginAPIHelp;
|
||||
|
||||
|
||||
/** Defines the {@link Intent} received which started the command. */
|
||||
public Intent commandIntent;
|
||||
|
||||
/** Defines if {@link ExecutionCommand} was started because of an external plugin request
|
||||
* like with an intent or from within Termux app itself. */
|
||||
public boolean isPluginExecutionCommand;
|
||||
|
||||
/** Defines the {@link ResultConfig} for the {@link ExecutionCommand} containing information
|
||||
* on how to handle the result. */
|
||||
public final ResultConfig resultConfig = new ResultConfig();
|
||||
|
||||
/** Defines the {@link ResultData} for the {@link ExecutionCommand} containing information
|
||||
* of the result. */
|
||||
public final ResultData resultData = new ResultData();
|
||||
|
||||
|
||||
/** Defines if processing results already called for this {@link ExecutionCommand}. */
|
||||
public boolean processingResultsAlreadyCalled;
|
||||
|
||||
private static final String LOG_TAG = "ExecutionCommand";
|
||||
|
||||
|
||||
public ExecutionCommand() {
|
||||
}
|
||||
|
||||
public ExecutionCommand(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ExecutionCommand(Integer id, String executable, String[] arguments, String stdin, String workingDirectory, boolean inBackground, boolean isFailsafe) {
|
||||
this.id = id;
|
||||
this.executable = executable;
|
||||
this.arguments = arguments;
|
||||
this.stdin = stdin;
|
||||
this.workingDirectory = workingDirectory;
|
||||
this.inBackground = inBackground;
|
||||
this.isFailsafe = isFailsafe;
|
||||
}
|
||||
|
||||
|
||||
public boolean isPluginExecutionCommandWithPendingResult() {
|
||||
return isPluginExecutionCommand && resultConfig.isCommandWithPendingResult();
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setState(ExecutionState newState) {
|
||||
// The state transition cannot go back or change if already at {@link ExecutionState#SUCCESS}
|
||||
if (newState.getValue() < currentState.getValue() || currentState == ExecutionState.SUCCESS) {
|
||||
Logger.logError(LOG_TAG, "Invalid "+ getCommandIdAndLabelLogString() + " state transition from \"" + currentState.getName() + "\" to " + "\"" + newState.getName() + "\"");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The {@link ExecutionState#FAILED} can be set again, like to add more errors, but we don't update
|
||||
// {@link #previousState} with the {@link #currentState} value if its at {@link ExecutionState#FAILED} to
|
||||
// preserve the last valid state
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
previousState = currentState;
|
||||
|
||||
currentState = newState;
|
||||
return true;
|
||||
}
|
||||
|
||||
public synchronized boolean hasExecuted() {
|
||||
return currentState.getValue() >= ExecutionState.EXECUTED.getValue();
|
||||
}
|
||||
|
||||
public synchronized boolean isExecuting() {
|
||||
return currentState == ExecutionState.EXECUTING;
|
||||
}
|
||||
|
||||
public synchronized boolean isSuccessful() {
|
||||
return currentState == ExecutionState.SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(null, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(null, code, message, throwablesList);
|
||||
}
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
if (!this.resultData.setStateFailed(type, code, message, throwablesList)) {
|
||||
Logger.logWarn(LOG_TAG, "setStateFailed for " + getCommandIdAndLabelLogString() + " resultData encountered an error.");
|
||||
}
|
||||
|
||||
return setState(ExecutionState.FAILED);
|
||||
}
|
||||
|
||||
public synchronized boolean shouldNotProcessResults() {
|
||||
if (processingResultsAlreadyCalled) {
|
||||
return true;
|
||||
} else {
|
||||
processingResultsAlreadyCalled = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isStateFailed() {
|
||||
if (currentState != ExecutionState.FAILED)
|
||||
return false;
|
||||
|
||||
if (!resultData.isStateFailed()) {
|
||||
Logger.logWarn(LOG_TAG, "The " + getCommandIdAndLabelLogString() + " has an invalid errCode value set in errors list while having ExecutionState.FAILED state.\n" + resultData.errorsList);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
if (!hasExecuted())
|
||||
return getExecutionInputLogString(this, true, true);
|
||||
else {
|
||||
return getExecutionOutputLogString(this, true, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ExecutionCommand} execution input parameters.
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @param logStdin Set to {@code true} if {@link #stdin} should be logged.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getExecutionInputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logStdin) {
|
||||
if (executionCommand == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
|
||||
|
||||
if (executionCommand.previousState != ExecutionState.PRE_EXECUTION)
|
||||
logString.append("\n").append(executionCommand.getPreviousStateLogString());
|
||||
logString.append("\n").append(executionCommand.getCurrentStateLogString());
|
||||
|
||||
logString.append("\n").append(executionCommand.getExecutableLogString());
|
||||
logString.append("\n").append(executionCommand.getArgumentsLogString());
|
||||
logString.append("\n").append(executionCommand.getWorkingDirectoryLogString());
|
||||
logString.append("\n").append(executionCommand.getInBackgroundLogString());
|
||||
logString.append("\n").append(executionCommand.getIsFailsafeLogString());
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
if (logStdin && (!ignoreNull || !DataUtils.isNullOrEmpty(executionCommand.stdin)))
|
||||
logString.append("\n").append(executionCommand.getStdinLogString());
|
||||
|
||||
if (!ignoreNull || executionCommand.backgroundCustomLogLevel != null)
|
||||
logString.append("\n").append(executionCommand.getBackgroundCustomLogLevelLogString());
|
||||
}
|
||||
|
||||
if (!ignoreNull || executionCommand.sessionAction != null)
|
||||
logString.append("\n").append(executionCommand.getSessionActionLogString());
|
||||
|
||||
if (!ignoreNull || executionCommand.commandIntent != null)
|
||||
logString.append("\n").append(executionCommand.getCommandIntentLogString());
|
||||
|
||||
logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString());
|
||||
if (executionCommand.isPluginExecutionCommand)
|
||||
logString.append("\n").append(ResultConfig.getResultConfigLogString(executionCommand.resultConfig, ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ExecutionCommand} execution output parameters.
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @param logResultData Set to {@code true} if {@link #resultData} should be logged.
|
||||
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr} should be logged.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getExecutionOutputLogString(final ExecutionCommand executionCommand, boolean ignoreNull, boolean logResultData, boolean logStdoutAndStderr) {
|
||||
if (executionCommand == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(executionCommand.getCommandIdAndLabelLogString()).append(":");
|
||||
|
||||
logString.append("\n").append(executionCommand.getPreviousStateLogString());
|
||||
logString.append("\n").append(executionCommand.getCurrentStateLogString());
|
||||
|
||||
if (logResultData)
|
||||
logString.append("\n").append(ResultData.getResultDataLogString(executionCommand.resultData, logStdoutAndStderr));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ExecutionCommand} with more details.
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getDetailedLogString(final ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(getExecutionInputLogString(executionCommand, false, true));
|
||||
logString.append(getExecutionOutputLogString(executionCommand, false, true, true));
|
||||
|
||||
logString.append("\n").append(executionCommand.getCommandDescriptionLogString());
|
||||
logString.append("\n").append(executionCommand.getCommandHelpLogString());
|
||||
logString.append("\n").append(executionCommand.getPluginAPIHelpLogString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ExecutionCommand}.
|
||||
*
|
||||
* @param executionCommand The {@link ExecutionCommand} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getExecutionCommandMarkdownString(final ExecutionCommand executionCommand) {
|
||||
if (executionCommand == null) return "null";
|
||||
|
||||
if (executionCommand.commandLabel == null) executionCommand.commandLabel = "Execution Command";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
markdownString.append("## ").append(executionCommand.commandLabel).append("\n");
|
||||
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Previous State", executionCommand.previousState.getName(), "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Current State", executionCommand.currentState.getName(), "-"));
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Executable", executionCommand.executable, "-"));
|
||||
markdownString.append("\n").append(getArgumentsMarkdownString(executionCommand.arguments));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Working Directory", executionCommand.workingDirectory, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("inBackground", executionCommand.inBackground, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isFailsafe", executionCommand.isFailsafe, "-"));
|
||||
|
||||
if (executionCommand.inBackground) {
|
||||
if (!DataUtils.isNullOrEmpty(executionCommand.stdin))
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdin", executionCommand.stdin, "-"));
|
||||
if (executionCommand.backgroundCustomLogLevel != null)
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Background Custom Log Level", executionCommand.backgroundCustomLogLevel, "-"));
|
||||
}
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-"));
|
||||
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-"));
|
||||
|
||||
markdownString.append("\n\n").append(ResultConfig.getResultConfigMarkdownString(executionCommand.resultConfig));
|
||||
|
||||
markdownString.append("\n\n").append(ResultData.getResultDataMarkdownString(executionCommand.resultData));
|
||||
|
||||
if (executionCommand.commandDescription != null || executionCommand.commandHelp != null) {
|
||||
if (executionCommand.commandDescription != null)
|
||||
markdownString.append("\n\n### Command Description\n\n").append(executionCommand.commandDescription).append("\n");
|
||||
if (executionCommand.commandHelp != null)
|
||||
markdownString.append("\n\n### Command Help\n\n").append(executionCommand.commandHelp).append("\n");
|
||||
markdownString.append("\n##\n");
|
||||
}
|
||||
|
||||
if (executionCommand.pluginAPIHelp != null) {
|
||||
markdownString.append("\n\n### Plugin API Help\n\n").append(executionCommand.pluginAPIHelp);
|
||||
markdownString.append("\n##\n");
|
||||
}
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
|
||||
public String getIdLogString() {
|
||||
if (id != null)
|
||||
return "(" + id + ") ";
|
||||
else
|
||||
return "";
|
||||
}
|
||||
|
||||
public String getCurrentStateLogString() {
|
||||
return "Current State: `" + currentState.getName() + "`";
|
||||
}
|
||||
|
||||
public String getPreviousStateLogString() {
|
||||
return "Previous State: `" + previousState.getName() + "`";
|
||||
}
|
||||
|
||||
public String getCommandLabelLogString() {
|
||||
if (commandLabel != null && !commandLabel.isEmpty())
|
||||
return commandLabel;
|
||||
else
|
||||
return "Execution Command";
|
||||
}
|
||||
|
||||
public String getCommandIdAndLabelLogString() {
|
||||
return getIdLogString() + getCommandLabelLogString();
|
||||
}
|
||||
|
||||
public String getExecutableLogString() {
|
||||
return "Executable: `" + executable + "`";
|
||||
}
|
||||
|
||||
public String getArgumentsLogString() {
|
||||
return getArgumentsLogString(arguments);
|
||||
}
|
||||
|
||||
public String getWorkingDirectoryLogString() {
|
||||
return "Working Directory: `" + workingDirectory + "`";
|
||||
}
|
||||
|
||||
public String getInBackgroundLogString() {
|
||||
return "inBackground: `" + inBackground + "`";
|
||||
}
|
||||
|
||||
public String getIsFailsafeLogString() {
|
||||
return "isFailsafe: `" + isFailsafe + "`";
|
||||
}
|
||||
|
||||
public String getStdinLogString() {
|
||||
if (DataUtils.isNullOrEmpty(stdin))
|
||||
return "Stdin: -";
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Stdin", stdin, "-");
|
||||
}
|
||||
|
||||
public String getBackgroundCustomLogLevelLogString() {
|
||||
return "Background Custom Log Level: `" + backgroundCustomLogLevel + "`";
|
||||
}
|
||||
|
||||
public String getSessionActionLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Session Action", sessionAction, "-");
|
||||
}
|
||||
|
||||
public String getCommandDescriptionLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Command Description", commandDescription, "-");
|
||||
}
|
||||
|
||||
public String getCommandHelpLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Command Help", commandHelp, "-");
|
||||
}
|
||||
|
||||
public String getPluginAPIHelpLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-");
|
||||
}
|
||||
|
||||
public String getCommandIntentLogString() {
|
||||
if (commandIntent == null)
|
||||
return "Command Intent: -";
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Command Intent", IntentUtils.getIntentString(commandIntent), "-");
|
||||
}
|
||||
|
||||
public String getIsPluginExecutionCommandLogString() {
|
||||
return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link List<String>} argumentsArray.
|
||||
* If argumentsArray are null or of size 0, then `Arguments: -` is returned. Otherwise
|
||||
* following format is returned:
|
||||
*
|
||||
* Arguments:
|
||||
* ```
|
||||
* Arg 1: `value`
|
||||
* Arg 2: 'value`
|
||||
* ```
|
||||
*
|
||||
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getArgumentsLogString(final String[] argumentsArray) {
|
||||
StringBuilder argumentsString = new StringBuilder("Arguments:");
|
||||
|
||||
if (argumentsArray != null && argumentsArray.length != 0) {
|
||||
argumentsString.append("\n```\n");
|
||||
for (int i = 0; i != argumentsArray.length; i++) {
|
||||
argumentsString.append(Logger.getSingleLineLogStringEntry("Arg " + (i + 1),
|
||||
DataUtils.getTruncatedCommandOutput(argumentsArray[i], Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, true, false, true),
|
||||
"-")).append("\n");
|
||||
}
|
||||
argumentsString.append("```");
|
||||
} else{
|
||||
argumentsString.append(" -");
|
||||
}
|
||||
|
||||
return argumentsString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link String[]} argumentsArray.
|
||||
* If argumentsArray are null or of size 0, then `**Arguments:** -` is returned. Otherwise
|
||||
* following format is returned:
|
||||
*
|
||||
* **Arguments:**
|
||||
*
|
||||
* **Arg 1:**
|
||||
* ```
|
||||
* value
|
||||
* ```
|
||||
* **Arg 2:**
|
||||
* ```
|
||||
* value
|
||||
*```
|
||||
*
|
||||
* @param argumentsArray The {@link String[]} argumentsArray to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getArgumentsMarkdownString(final String[] argumentsArray) {
|
||||
StringBuilder argumentsString = new StringBuilder("**Arguments:**");
|
||||
|
||||
if (argumentsArray != null && argumentsArray.length != 0) {
|
||||
argumentsString.append("\n");
|
||||
for (int i = 0; i != argumentsArray.length; i++) {
|
||||
argumentsString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Arg " + (i + 1), argumentsArray[i], "-")).append("\n");
|
||||
}
|
||||
} else{
|
||||
argumentsString.append(" - ");
|
||||
}
|
||||
|
||||
return argumentsString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.termux.shared.models;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An object that stored info for {@link com.termux.shared.activities.ReportActivity}.
|
||||
*/
|
||||
public class ReportInfo implements Serializable {
|
||||
|
||||
/**
|
||||
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
|
||||
*
|
||||
* Like when calling `Bundle.getSerializable()` on Android.
|
||||
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
|
||||
* `java.io.InvalidClassException: <class_name>; local class incompatible`
|
||||
*
|
||||
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
|
||||
* app is compiled, even if its kept during library compilation.
|
||||
*
|
||||
* **See Also:**
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
|
||||
*/
|
||||
@Keep
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
|
||||
/** The user action that was being processed for which the report was generated. */
|
||||
public final String userAction;
|
||||
/** The internal app component that sent the report. */
|
||||
public final String sender;
|
||||
/** The report title. */
|
||||
public final String reportTitle;
|
||||
/** The markdown report text prefix. Will not be part of copy and share operations, etc. */
|
||||
public String reportStringPrefix;
|
||||
/** The markdown report text. */
|
||||
public String reportString;
|
||||
/** The markdown report text suffix. Will not be part of copy and share operations, etc. */
|
||||
public String reportStringSuffix;
|
||||
/** If set to {@code true}, then report, app and device info will be added to the report when
|
||||
* markdown is generated.
|
||||
*/
|
||||
public final boolean addReportInfoHeaderToMarkdown;
|
||||
/** The timestamp for the report. */
|
||||
public final String reportTimestamp;
|
||||
|
||||
/** The label for the report file to save if user selects menu_item_save_report_to_file. */
|
||||
public final String reportSaveFileLabel;
|
||||
/** The path for the report file to save if user selects menu_item_save_report_to_file. */
|
||||
public final String reportSaveFilePath;
|
||||
|
||||
public ReportInfo(String userAction, String sender, String reportTitle, String reportStringPrefix,
|
||||
String reportString, String reportStringSuffix, boolean addReportInfoHeaderToMarkdown,
|
||||
String reportSaveFileLabel, String reportSaveFilePath) {
|
||||
this.userAction = userAction;
|
||||
this.sender = sender;
|
||||
this.reportTitle = reportTitle;
|
||||
this.reportStringPrefix = reportStringPrefix;
|
||||
this.reportString = reportString;
|
||||
this.reportStringSuffix = reportStringSuffix;
|
||||
this.addReportInfoHeaderToMarkdown = addReportInfoHeaderToMarkdown;
|
||||
this.reportSaveFileLabel = reportSaveFileLabel;
|
||||
this.reportSaveFilePath = reportSaveFilePath;
|
||||
this.reportTimestamp = AndroidUtils.getCurrentMilliSecondUTCTimeStamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ReportInfo}.
|
||||
*
|
||||
* @param reportInfo The {@link ReportInfo} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getReportInfoMarkdownString(final ReportInfo reportInfo) {
|
||||
if (reportInfo == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (reportInfo.addReportInfoHeaderToMarkdown) {
|
||||
markdownString.append("## Report Info\n\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("User Action", reportInfo.userAction, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Sender", reportInfo.sender, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Report Timestamp", reportInfo.reportTimestamp, "-"));
|
||||
markdownString.append("\n##\n\n");
|
||||
}
|
||||
|
||||
markdownString.append(reportInfo.reportString);
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
package com.termux.shared.models;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
|
||||
import java.util.Formatter;
|
||||
|
||||
public class ResultConfig {
|
||||
|
||||
/** Defines {@link PendingIntent} that should be sent with the result of the command. We cannot
|
||||
* implement {@link java.io.Serializable} because {@link PendingIntent} cannot be serialized. */
|
||||
public PendingIntent resultPendingIntent;
|
||||
/** The key with which to send result {@link android.os.Bundle} in {@link #resultPendingIntent}. */
|
||||
public String resultBundleKey;
|
||||
/** The key with which to send {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||
public String resultStdoutKey;
|
||||
/** The key with which to send {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||
public String resultStderrKey;
|
||||
/** The key with which to send {@link ResultData#exitCode} in {@link #resultPendingIntent}. */
|
||||
public String resultExitCodeKey;
|
||||
/** The key with which to send {@link ResultData#errorsList} errCode in {@link #resultPendingIntent}. */
|
||||
public String resultErrCodeKey;
|
||||
/** The key with which to send {@link ResultData#errorsList} errmsg in {@link #resultPendingIntent}. */
|
||||
public String resultErrmsgKey;
|
||||
/** The key with which to send original length of {@link ResultData#stdout} in {@link #resultPendingIntent}. */
|
||||
public String resultStdoutOriginalLengthKey;
|
||||
/** The key with which to send original length of {@link ResultData#stderr} in {@link #resultPendingIntent}. */
|
||||
public String resultStderrOriginalLengthKey;
|
||||
|
||||
|
||||
/** Defines the directory path in which to write the result of the command. */
|
||||
public String resultDirectoryPath;
|
||||
/** Defines the directory path under which {@link #resultDirectoryPath} can exist. */
|
||||
public String resultDirectoryAllowedParentPath;
|
||||
/** Defines whether the result should be written to a single file or multiple files
|
||||
* (err, error, stdout, stderr, exit_code) in {@link #resultDirectoryPath}. */
|
||||
public boolean resultSingleFile;
|
||||
/** Defines the basename of the result file that should be created in {@link #resultDirectoryPath}
|
||||
* if {@link #resultSingleFile} is {@code true}. */
|
||||
public String resultFileBasename;
|
||||
/** Defines the output {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||
public String resultFileOutputFormat;
|
||||
/** Defines the error {@link Formatter} format of the {@link #resultFileBasename} result file. */
|
||||
public String resultFileErrorFormat;
|
||||
/** Defines the suffix of the result files that should be created in {@link #resultDirectoryPath}
|
||||
* if {@link #resultSingleFile} is {@code true}. */
|
||||
public String resultFilesSuffix;
|
||||
|
||||
|
||||
public ResultConfig() {
|
||||
}
|
||||
|
||||
|
||||
public boolean isCommandWithPendingResult() {
|
||||
return resultPendingIntent != null || resultDirectoryPath != null;
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getResultConfigLogString(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ResultConfig} parameters.
|
||||
*
|
||||
* @param resultConfig The {@link ResultConfig} to convert.
|
||||
* @param ignoreNull Set to {@code true} if non-critical {@code null} values are to be ignored.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getResultConfigLogString(final ResultConfig resultConfig, boolean ignoreNull) {
|
||||
if (resultConfig == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append("Result Pending: `").append(resultConfig.isCommandWithPendingResult()).append("`\n");
|
||||
|
||||
if (resultConfig.resultPendingIntent != null) {
|
||||
logString.append(resultConfig.getResultPendingIntentVariablesLogString(ignoreNull));
|
||||
if (resultConfig.resultDirectoryPath != null)
|
||||
logString.append("\n");
|
||||
}
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null && !resultConfig.resultDirectoryPath.isEmpty())
|
||||
logString.append(resultConfig.getResultDirectoryVariablesLogString(ignoreNull));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
public String getResultPendingIntentVariablesLogString(boolean ignoreNull) {
|
||||
if (resultPendingIntent == null) return "Result PendingIntent Creator: -";
|
||||
|
||||
StringBuilder resultPendingIntentVariablesString = new StringBuilder();
|
||||
|
||||
resultPendingIntentVariablesString.append("Result PendingIntent Creator: `").append(resultPendingIntent.getCreatorPackage()).append("`");
|
||||
|
||||
if (!ignoreNull || resultBundleKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Bundle Key", resultBundleKey, "-"));
|
||||
if (!ignoreNull || resultStdoutKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Key", resultStdoutKey, "-"));
|
||||
if (!ignoreNull || resultStderrKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Key", resultStderrKey, "-"));
|
||||
if (!ignoreNull || resultExitCodeKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Exit Code Key", resultExitCodeKey, "-"));
|
||||
if (!ignoreNull || resultErrCodeKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Err Code Key", resultErrCodeKey, "-"));
|
||||
if (!ignoreNull || resultErrmsgKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Error Key", resultErrmsgKey, "-"));
|
||||
if (!ignoreNull || resultStdoutOriginalLengthKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stdout Original Length Key", resultStdoutOriginalLengthKey, "-"));
|
||||
if (!ignoreNull || resultStderrOriginalLengthKey != null)
|
||||
resultPendingIntentVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Stderr Original Length Key", resultStderrOriginalLengthKey, "-"));
|
||||
|
||||
return resultPendingIntentVariablesString.toString();
|
||||
}
|
||||
|
||||
public String getResultDirectoryVariablesLogString(boolean ignoreNull) {
|
||||
if (resultDirectoryPath == null) return "Result Directory Path: -";
|
||||
|
||||
StringBuilder resultDirectoryVariablesString = new StringBuilder();
|
||||
|
||||
resultDirectoryVariablesString.append(Logger.getSingleLineLogStringEntry("Result Directory Path", resultDirectoryPath, "-"));
|
||||
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Single File", resultSingleFile, "-"));
|
||||
if (!ignoreNull || resultFileBasename != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Basename", resultFileBasename, "-"));
|
||||
if (!ignoreNull || resultFileOutputFormat != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Output Format", resultFileOutputFormat, "-"));
|
||||
if (!ignoreNull || resultFileErrorFormat != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result File Error Format", resultFileErrorFormat, "-"));
|
||||
if (!ignoreNull || resultFilesSuffix != null)
|
||||
resultDirectoryVariablesString.append("\n").append(Logger.getSingleLineLogStringEntry("Result Files Suffix", resultFilesSuffix, "-"));
|
||||
|
||||
return resultDirectoryVariablesString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ResultConfig}.
|
||||
*
|
||||
* @param resultConfig The {@link ResultConfig} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getResultConfigMarkdownString(final ResultConfig resultConfig) {
|
||||
if (resultConfig == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultConfig.resultPendingIntent != null)
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result PendingIntent Creator", resultConfig.resultPendingIntent.getCreatorPackage(), "-"));
|
||||
else
|
||||
markdownString.append("**Result PendingIntent Creator:** - ");
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null) {
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Directory Path", resultConfig.resultDirectoryPath, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Single File", resultConfig.resultSingleFile, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Basename", resultConfig.resultFileBasename, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Output Format", resultConfig.resultFileOutputFormat, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result File Error Format", resultConfig.resultFileErrorFormat, "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Result Files Suffix", resultConfig.resultFilesSuffix, "-"));
|
||||
}
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
package com.termux.shared.models;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class ResultData implements Serializable {
|
||||
|
||||
/** The stdout of command. */
|
||||
public final StringBuilder stdout = new StringBuilder();
|
||||
/** The stderr of command. */
|
||||
public final StringBuilder stderr = new StringBuilder();
|
||||
/** The exit code of command. */
|
||||
public Integer exitCode;
|
||||
|
||||
/** The internal errors list of command. */
|
||||
public List<Error> errorsList = new ArrayList<>();
|
||||
|
||||
|
||||
public ResultData() {
|
||||
}
|
||||
|
||||
|
||||
public void clearStdout() {
|
||||
stdout.setLength(0);
|
||||
}
|
||||
|
||||
public StringBuilder prependStdout(String message) {
|
||||
return stdout.insert(0, message);
|
||||
}
|
||||
|
||||
public StringBuilder prependStdoutLn(String message) {
|
||||
return stdout.insert(0, message + "\n");
|
||||
}
|
||||
|
||||
public StringBuilder appendStdout(String message) {
|
||||
return stdout.append(message);
|
||||
}
|
||||
|
||||
public StringBuilder appendStdoutLn(String message) {
|
||||
return stdout.append(message).append("\n");
|
||||
}
|
||||
|
||||
|
||||
public void clearStderr() {
|
||||
stderr.setLength(0);
|
||||
}
|
||||
|
||||
public StringBuilder prependStderr(String message) {
|
||||
return stderr.insert(0, message);
|
||||
}
|
||||
|
||||
public StringBuilder prependStderrLn(String message) {
|
||||
return stderr.insert(0, message + "\n");
|
||||
}
|
||||
|
||||
public StringBuilder appendStderr(String message) {
|
||||
return stderr.append(message);
|
||||
}
|
||||
|
||||
public StringBuilder appendStderrLn(String message) {
|
||||
return stderr.append(message).append("\n");
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(null, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(null, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
if (errorsList == null)
|
||||
errorsList = new ArrayList<>();
|
||||
|
||||
Error error = new Error();
|
||||
errorsList.add(error);
|
||||
|
||||
return error.setStateFailed(type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public boolean isStateFailed() {
|
||||
if (errorsList != null) {
|
||||
for (Error error : errorsList)
|
||||
if (error.isStateFailed())
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int getErrCode() {
|
||||
if (errorsList != null && errorsList.size() > 0)
|
||||
return errorsList.get(errorsList.size() - 1).getCode();
|
||||
else
|
||||
return Errno.ERRNO_SUCCESS.getCode();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getResultDataLogString(this, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link ResultData} parameters.
|
||||
*
|
||||
* @param resultData The {@link ResultData} to convert.
|
||||
* @param logStdoutAndStderr Set to {@code true} if {@link #stdout} and {@link #stderr} should be logged.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getResultDataLogString(final ResultData resultData, boolean logStdoutAndStderr) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
if (logStdoutAndStderr) {
|
||||
logString.append("\n").append(resultData.getStdoutLogString());
|
||||
logString.append("\n").append(resultData.getStderrLogString());
|
||||
}
|
||||
logString.append("\n").append(resultData.getExitCodeLogString());
|
||||
|
||||
logString.append("\n\n").append(getErrorsListLogString(resultData));
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public String getStdoutLogString() {
|
||||
if (stdout.toString().isEmpty())
|
||||
return Logger.getSingleLineLogStringEntry("Stdout", null, "-");
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Stdout", DataUtils.getTruncatedCommandOutput(stdout.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||
}
|
||||
|
||||
public String getStderrLogString() {
|
||||
if (stderr.toString().isEmpty())
|
||||
return Logger.getSingleLineLogStringEntry("Stderr", null, "-");
|
||||
else
|
||||
return Logger.getMultiLineLogStringEntry("Stderr", DataUtils.getTruncatedCommandOutput(stderr.toString(), Logger.LOGGER_ENTRY_MAX_SAFE_PAYLOAD / 5, false, false, true), "-");
|
||||
}
|
||||
|
||||
public String getExitCodeLogString() {
|
||||
return Logger.getSingleLineLogStringEntry("Exit Code", exitCode, "-");
|
||||
}
|
||||
|
||||
public static String getErrorsListLogString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!logString.toString().isEmpty())
|
||||
logString.append("\n");
|
||||
logString.append(Error.getErrorLogString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link ResultData}.
|
||||
*
|
||||
* @param resultData The {@link ResultData} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getResultDataMarkdownString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultData.stdout.toString().isEmpty())
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stdout", null, "-"));
|
||||
else
|
||||
markdownString.append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", resultData.stdout.toString(), "-"));
|
||||
|
||||
if (resultData.stderr.toString().isEmpty())
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Stderr", null, "-"));
|
||||
else
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", resultData.stderr.toString(), "-"));
|
||||
|
||||
markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", resultData.exitCode, "-"));
|
||||
|
||||
markdownString.append("\n\n").append(getErrorsListMarkdownString(resultData));
|
||||
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
public static String getErrorsListMarkdownString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!markdownString.toString().isEmpty())
|
||||
markdownString.append("\n");
|
||||
markdownString.append(Error.getErrorMarkdownString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
public static String getErrorsListMinimalString(final ResultData resultData) {
|
||||
if (resultData == null) return "null";
|
||||
|
||||
StringBuilder minimalString = new StringBuilder();
|
||||
|
||||
if (resultData.errorsList != null) {
|
||||
for (Error error : resultData.errorsList) {
|
||||
if (error.isStateFailed()) {
|
||||
if (!minimalString.toString().isEmpty())
|
||||
minimalString.append("\n");
|
||||
minimalString.append(Error.getMinimalErrorString(error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return minimalString.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,254 @@
|
|||
package com.termux.shared.models;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.activities.TextIOActivity;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* An object that stored info for {@link TextIOActivity}.
|
||||
* Max text limit is 95KB to prevent TransactionTooLargeException as per
|
||||
* {@link DataUtils#TRANSACTION_SIZE_LIMIT_IN_BYTES}. Larger size can be supported for in-app
|
||||
* transactions by storing {@link TextIOInfo} as a serialized object in a file like
|
||||
* {@link com.termux.shared.activities.ReportActivity} does.
|
||||
*/
|
||||
public class TextIOInfo implements Serializable {
|
||||
|
||||
/**
|
||||
* Explicitly define `serialVersionUID` to prevent exceptions on deserialization.
|
||||
*
|
||||
* Like when calling `Bundle.getSerializable()` on Android.
|
||||
* `android.os.BadParcelableException: Parcelable encountered IOException reading a Serializable object` (name = <class_name>)
|
||||
* `java.io.InvalidClassException: <class_name>; local class incompatible`
|
||||
*
|
||||
* The `@Keep` annotation is necessary to prevent the field from being removed by proguard when
|
||||
* app is compiled, even if its kept during library compilation.
|
||||
*
|
||||
* **See Also:**
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/version.html#a6678
|
||||
* - https://docs.oracle.com/javase/8/docs/platform/serialization/spec/class.html#a4100
|
||||
*/
|
||||
@Keep
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
|
||||
public static final int GENERAL_DATA_SIZE_LIMIT_IN_BYTES = 1000;
|
||||
public static final int LABEL_SIZE_LIMIT_IN_BYTES = 4000;
|
||||
public static final int TEXT_SIZE_LIMIT_IN_BYTES = 100000 - GENERAL_DATA_SIZE_LIMIT_IN_BYTES - LABEL_SIZE_LIMIT_IN_BYTES; // < 100KB
|
||||
|
||||
/** The action for which {@link TextIOActivity} will be started. */
|
||||
private final String mAction;
|
||||
/** The internal app component that is will start the {@link TextIOActivity}. */
|
||||
private final String mSender;
|
||||
|
||||
/** The activity title. */
|
||||
private String mTitle;
|
||||
|
||||
/** If back button should be shown in {@link android.app.ActionBar}. */
|
||||
private boolean mShowBackButtonInActionBar = false;
|
||||
|
||||
|
||||
/** If label is enabled. */
|
||||
private boolean mLabelEnabled = false;
|
||||
/**
|
||||
* The label of text input set in {@link android.widget.TextView} that can be updated by user.
|
||||
* Max allowed length is {@link #LABEL_SIZE_LIMIT_IN_BYTES}.
|
||||
*/
|
||||
private String mLabel;
|
||||
/** The text size of label. Defaults to 14sp. */
|
||||
private int mLabelSize = 14;
|
||||
/** The text color of label. Defaults to {@link Color#BLACK}. */
|
||||
private int mLabelColor = Color.BLACK;
|
||||
/** The {@link Typeface} family of label. Defaults to "sans-serif". */
|
||||
private String mLabelTypeFaceFamily = "sans-serif";
|
||||
/** The {@link Typeface} style of label. Defaults to {@link Typeface#BOLD}. */
|
||||
private int mLabelTypeFaceStyle = Typeface.BOLD;
|
||||
|
||||
|
||||
/**
|
||||
* The text of text input set in {@link android.widget.EditText} that can be updated by user.
|
||||
* Max allowed length is {@link #TEXT_SIZE_LIMIT_IN_BYTES}.
|
||||
*/
|
||||
private String mText;
|
||||
/** The text size for text. Defaults to 12sp. */
|
||||
private int mTextSize = 12;
|
||||
/** The text size for text. Defaults to {@link #TEXT_SIZE_LIMIT_IN_BYTES}. */
|
||||
private int mTextLengthLimit = TEXT_SIZE_LIMIT_IN_BYTES;
|
||||
/** The text color of text. Defaults to {@link Color#BLACK}. */
|
||||
private int mTextColor = Color.BLACK;
|
||||
/** The {@link Typeface} family for text. Defaults to "sans-serif". */
|
||||
private String mTextTypeFaceFamily = "sans-serif";
|
||||
/** The {@link Typeface} style for text. Defaults to {@link Typeface#NORMAL}. */
|
||||
private int mTextTypeFaceStyle = Typeface.NORMAL;
|
||||
/** If horizontal scrolling should be enabled for text. */
|
||||
private boolean mTextHorizontallyScrolling = false;
|
||||
/** If character usage should be enabled for text. */
|
||||
private boolean mShowTextCharacterUsage = false;
|
||||
/** If editing text should be disabled so that text acts like its in a {@link android.widget.TextView}. */
|
||||
private boolean mEditingTextDisabled = false;
|
||||
|
||||
|
||||
public TextIOInfo(@NonNull String action, @NonNull String sender) {
|
||||
mAction = action;
|
||||
mSender = sender;
|
||||
}
|
||||
|
||||
|
||||
public String getAction() {
|
||||
return mAction;
|
||||
}
|
||||
|
||||
public String getSender() {
|
||||
return mSender;
|
||||
}
|
||||
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
mTitle = title;
|
||||
}
|
||||
|
||||
public boolean shouldShowBackButtonInActionBar() {
|
||||
return mShowBackButtonInActionBar;
|
||||
}
|
||||
|
||||
public void setShowBackButtonInActionBar(boolean showBackButtonInActionBar) {
|
||||
mShowBackButtonInActionBar = showBackButtonInActionBar;
|
||||
}
|
||||
|
||||
|
||||
public boolean isLabelEnabled() {
|
||||
return mLabelEnabled;
|
||||
}
|
||||
|
||||
public void setLabelEnabled(boolean labelEnabled) {
|
||||
mLabelEnabled = labelEnabled;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return mLabel;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
mLabel = DataUtils.getTruncatedCommandOutput(label, LABEL_SIZE_LIMIT_IN_BYTES, true, false, false);
|
||||
}
|
||||
|
||||
public int getLabelSize() {
|
||||
return mLabelSize;
|
||||
}
|
||||
|
||||
public void setLabelSize(int labelSize) {
|
||||
if (labelSize > 0)
|
||||
mLabelSize = labelSize;
|
||||
}
|
||||
|
||||
public int getLabelColor() {
|
||||
return mLabelColor;
|
||||
}
|
||||
|
||||
public void setLabelColor(int labelColor) {
|
||||
mLabelColor = labelColor;
|
||||
}
|
||||
|
||||
public String getLabelTypeFaceFamily() {
|
||||
return mLabelTypeFaceFamily;
|
||||
}
|
||||
|
||||
public void setLabelTypeFaceFamily(String labelTypeFaceFamily) {
|
||||
mLabelTypeFaceFamily = labelTypeFaceFamily;
|
||||
}
|
||||
|
||||
public int getLabelTypeFaceStyle() {
|
||||
return mLabelTypeFaceStyle;
|
||||
}
|
||||
|
||||
public void setLabelTypeFaceStyle(int labelTypeFaceStyle) {
|
||||
mLabelTypeFaceStyle = labelTypeFaceStyle;
|
||||
}
|
||||
|
||||
|
||||
public String getText() {
|
||||
return mText;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
mText = DataUtils.getTruncatedCommandOutput(text, TEXT_SIZE_LIMIT_IN_BYTES, true, false, false);
|
||||
}
|
||||
|
||||
public int getTextSize() {
|
||||
return mTextSize;
|
||||
}
|
||||
|
||||
public void setTextSize(int textSize) {
|
||||
if (textSize > 0)
|
||||
mTextSize = textSize;
|
||||
}
|
||||
|
||||
public int getTextLengthLimit() {
|
||||
return mTextLengthLimit;
|
||||
}
|
||||
|
||||
public void setTextLengthLimit(int textLengthLimit) {
|
||||
if (textLengthLimit < TEXT_SIZE_LIMIT_IN_BYTES)
|
||||
mTextLengthLimit = textLengthLimit;
|
||||
}
|
||||
|
||||
public int getTextColor() {
|
||||
return mTextColor;
|
||||
}
|
||||
|
||||
public void setTextColor(int textColor) {
|
||||
mTextColor = textColor;
|
||||
}
|
||||
|
||||
public String getTextTypeFaceFamily() {
|
||||
return mTextTypeFaceFamily;
|
||||
}
|
||||
|
||||
public void setTextTypeFaceFamily(String textTypeFaceFamily) {
|
||||
mTextTypeFaceFamily = textTypeFaceFamily;
|
||||
}
|
||||
|
||||
public int getTextTypeFaceStyle() {
|
||||
return mTextTypeFaceStyle;
|
||||
}
|
||||
|
||||
public void setTextTypeFaceStyle(int textTypeFaceStyle) {
|
||||
mTextTypeFaceStyle = textTypeFaceStyle;
|
||||
}
|
||||
|
||||
public boolean isHorizontallyScrollable() {
|
||||
return mTextHorizontallyScrolling;
|
||||
}
|
||||
|
||||
public void setTextHorizontallyScrolling(boolean textHorizontallyScrolling) {
|
||||
mTextHorizontallyScrolling = textHorizontallyScrolling;
|
||||
}
|
||||
|
||||
public boolean shouldShowTextCharacterUsage() {
|
||||
return mShowTextCharacterUsage;
|
||||
}
|
||||
|
||||
public void setShowTextCharacterUsage(boolean showTextCharacterUsage) {
|
||||
mShowTextCharacterUsage = showTextCharacterUsage;
|
||||
}
|
||||
|
||||
public boolean isEditingTextDisabled() {
|
||||
return mEditingTextDisabled;
|
||||
}
|
||||
|
||||
public void setEditingTextDisabled(boolean editingTextDisabled) {
|
||||
mEditingTextDisabled = editingTextDisabled;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
package com.termux.shared.models.errors;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
/** The {@link Class} that defines error messages and codes. */
|
||||
public class Errno {
|
||||
|
||||
private static final HashMap<String, Errno> map = new HashMap<>();
|
||||
|
||||
public static final String TYPE = "Error";
|
||||
|
||||
|
||||
public static final Errno ERRNO_SUCCESS = new Errno(TYPE, Activity.RESULT_OK, "Success");
|
||||
public static final Errno ERRNO_CANCELLED = new Errno(TYPE, Activity.RESULT_CANCELED, "Cancelled");
|
||||
public static final Errno ERRNO_MINOR_FAILURES = new Errno(TYPE, Activity.RESULT_FIRST_USER, "Minor failure");
|
||||
public static final Errno ERRNO_FAILED = new Errno(TYPE, Activity.RESULT_FIRST_USER + 1, "Failed");
|
||||
|
||||
/** The errno type. */
|
||||
protected String type;
|
||||
/** The errno code. */
|
||||
protected final int code;
|
||||
/** The errno message. */
|
||||
protected final String message;
|
||||
|
||||
private static final String LOG_TAG = "Errno";
|
||||
|
||||
|
||||
public Errno(final String type, final int code, final String message) {
|
||||
this.type = type;
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
map.put(type + ":" + code, this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "type=" + type + ", code=" + code + ", message=\"" + message + "\"";
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Errno} of a specific type and code.
|
||||
*
|
||||
* @param type The unique type of the {@link Errno}.
|
||||
* @param code The unique code of the {@link Errno}.
|
||||
*/
|
||||
public static Errno valueOf(String type, Integer code) {
|
||||
if (type == null || type.isEmpty() || code == null) return null;
|
||||
return map.get(type + ":" + code);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public Error getError() {
|
||||
return new Error(getType(), getCode(), getMessage());
|
||||
}
|
||||
|
||||
public Error getError(Object... args) {
|
||||
try {
|
||||
return new Error(getType(), getCode(), String.format(getMessage(), args));
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
|
||||
// Return unformatted message as a backup
|
||||
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args));
|
||||
}
|
||||
}
|
||||
|
||||
public Error getError(Throwable throwable, Object... args) {
|
||||
if (throwable == null)
|
||||
return getError(args);
|
||||
else
|
||||
return getError(Collections.singletonList(throwable), args);
|
||||
}
|
||||
|
||||
public Error getError(List<Throwable> throwablesList, Object... args) {
|
||||
try {
|
||||
if (throwablesList == null)
|
||||
return new Error(getType(), getCode(), String.format(getMessage(), args));
|
||||
else
|
||||
return new Error(getType(), getCode(), String.format(getMessage(), args), throwablesList);
|
||||
} catch (Exception e) {
|
||||
Logger.logWarn(LOG_TAG, "Exception raised while calling String.format() for error message of errno " + this + " with args" + Arrays.toString(args) + "\n" + e.getMessage());
|
||||
// Return unformatted message as a backup
|
||||
return new Error(getType(), getCode(), getMessage() + ": " + Arrays.toString(args), throwablesList);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
package com.termux.shared.models.errors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class Error implements Serializable {
|
||||
|
||||
/** The optional error label. */
|
||||
private String label;
|
||||
/** The error type. */
|
||||
private String type;
|
||||
/** The error code. */
|
||||
private int code;
|
||||
/** The error message. */
|
||||
private String message;
|
||||
/** The error exceptions. */
|
||||
private List<Throwable> throwablesList = new ArrayList<>();
|
||||
|
||||
private static final String LOG_TAG = "Error";
|
||||
|
||||
|
||||
public Error() {
|
||||
InitError(null, null, null, null);
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||
InitError(type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message, Throwable throwable) {
|
||||
InitError(type, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(String type, Integer code, String message) {
|
||||
InitError(type, code, message, null);
|
||||
}
|
||||
|
||||
public Error(Integer code, String message, List<Throwable> throwablesList) {
|
||||
InitError(null, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(Integer code, String message, Throwable throwable) {
|
||||
InitError(null, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(Integer code, String message) {
|
||||
InitError(null, code, message, null);
|
||||
}
|
||||
|
||||
public Error(String message, Throwable throwable) {
|
||||
InitError(null, null, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public Error(String message, List<Throwable> throwablesList) {
|
||||
InitError(null, null, message, throwablesList);
|
||||
}
|
||||
|
||||
public Error(String message) {
|
||||
InitError(null, null, message, null);
|
||||
}
|
||||
|
||||
private void InitError(String type, Integer code, String message, List<Throwable> throwablesList) {
|
||||
if (type != null && !type.isEmpty())
|
||||
this.type = type;
|
||||
else
|
||||
this.type = Errno.TYPE;
|
||||
|
||||
if (code != null && code > Errno.ERRNO_SUCCESS.getCode())
|
||||
this.code = code;
|
||||
else
|
||||
this.code = Errno.ERRNO_SUCCESS.getCode();
|
||||
|
||||
this.message = message;
|
||||
|
||||
if (throwablesList != null)
|
||||
this.throwablesList = throwablesList;
|
||||
}
|
||||
|
||||
public Error setLabel(String label) {
|
||||
this.label = label;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void prependMessage(String message) {
|
||||
if (message != null && isStateFailed())
|
||||
this.message = message + this.message;
|
||||
}
|
||||
|
||||
public void appendMessage(String message) {
|
||||
if (message != null && isStateFailed())
|
||||
this.message = this.message + message;
|
||||
}
|
||||
|
||||
public List<Throwable> getThrowablesList() {
|
||||
return Collections.unmodifiableList(throwablesList);
|
||||
}
|
||||
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, Throwable throwable) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), Collections.singletonList(throwable));
|
||||
}
|
||||
public synchronized boolean setStateFailed(@NonNull Error error, List<Throwable> throwablesList) {
|
||||
return setStateFailed(error.getType(), error.getCode(), error.getMessage(), throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message) {
|
||||
return setStateFailed(this.type, code, message, null);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, Throwable throwable) {
|
||||
return setStateFailed(this.type, code, message, Collections.singletonList(throwable));
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(int code, String message, List<Throwable> throwablesList) {
|
||||
return setStateFailed(this.type, code, message, throwablesList);
|
||||
}
|
||||
|
||||
public synchronized boolean setStateFailed(String type, int code, String message, List<Throwable> throwablesList) {
|
||||
this.message = message;
|
||||
this.throwablesList = throwablesList;
|
||||
|
||||
if (type != null && !type.isEmpty())
|
||||
this.type = type;
|
||||
|
||||
if (code > Errno.ERRNO_SUCCESS.getCode()) {
|
||||
this.code = code;
|
||||
return true;
|
||||
} else {
|
||||
Logger.logWarn(LOG_TAG, "Ignoring invalid error code value \"" + code + "\". Force setting it to RESULT_CODE_FAILED \"" + Errno.ERRNO_FAILED.getCode() + "\"");
|
||||
this.code = Errno.ERRNO_FAILED.getCode();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isStateFailed() {
|
||||
return code > Errno.ERRNO_SUCCESS.getCode();
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getErrorLogString(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a log friendly {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getErrorLogString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(error.getCodeString());
|
||||
logString.append("\n").append(error.getTypeAndMessageLogString());
|
||||
if (error.throwablesList != null)
|
||||
logString.append("\n").append(error.geStackTracesLogString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a minimal log friendly {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the log friendly {@link String}.
|
||||
*/
|
||||
public static String getMinimalErrorLogString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append(error.getCodeString());
|
||||
logString.append(error.getTypeAndMessageLogString());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a minimal {@link String} for {@link Error} error parameters.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the {@link String}.
|
||||
*/
|
||||
public static String getMinimalErrorString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder logString = new StringBuilder();
|
||||
|
||||
logString.append("(").append(error.getCode()).append(") ");
|
||||
logString.append(error.getType()).append(": ").append(error.getMessage());
|
||||
|
||||
return logString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for {@link Error}.
|
||||
*
|
||||
* @param error The {@link Error} to convert.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getErrorMarkdownString(final Error error) {
|
||||
if (error == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
markdownString.append(MarkdownUtils.getSingleLineMarkdownStringEntry("Error Code", error.getCode(), "-"));
|
||||
markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry((Errno.TYPE.equals(error.getType()) ? "Error Message" : "Error Message (" + error.getType() + ")"), error.message, "-"));
|
||||
markdownString.append("\n\n").append(error.geStackTracesMarkdownString());
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
|
||||
public String getCodeString() {
|
||||
return Logger.getSingleLineLogStringEntry("Error Code", code, "-");
|
||||
}
|
||||
|
||||
public String getTypeAndMessageLogString() {
|
||||
return Logger.getMultiLineLogStringEntry(Errno.TYPE.equals(type) ? "Error Message" : "Error Message (" + type + ")", message, "-");
|
||||
}
|
||||
|
||||
public String geStackTracesLogString() {
|
||||
return Logger.getStackTracesString("StackTraces:", Logger.getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
public String geStackTracesMarkdownString() {
|
||||
return Logger.getStackTracesMarkdownString("StackTraces", Logger.getStackTracesStringArray(throwablesList));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.termux.shared.models.errors;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/** The {@link Class} that defines FileUtils error messages and codes. */
|
||||
public class FileUtilsErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "FileUtils Error";
|
||||
|
||||
|
||||
/* Errors for null or empty paths (100-150) */
|
||||
public static final Errno ERRNO_EXECUTABLE_REQUIRED = new Errno(TYPE, 100, "Executable required.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE_PATH = new Errno(TYPE, 101, "The regular file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_REGULAR_FILE = new Errno(TYPE, 102, "The regular file is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE_PATH = new Errno(TYPE, 103, "The executable file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_EXECUTABLE_FILE = new Errno(TYPE, 104, "The executable file is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE_PATH = new Errno(TYPE, 105, "The directory file path is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_DIRECTORY_FILE = new Errno(TYPE, 106, "The directory file is null or empty.");
|
||||
|
||||
|
||||
|
||||
/* Errors for invalid or not found files at path (150-200) */
|
||||
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH = new Errno(TYPE, 150, "The %1$s not found at path \"%2$s\".");
|
||||
public static final Errno ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT = new Errno(TYPE, 151, "The %1$s not found at path.");
|
||||
|
||||
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND = new Errno(TYPE, 152, "Non-regular file found at %1$s path \"%2$s\".");
|
||||
public static final Errno ERRNO_NON_REGULAR_FILE_FOUND_SHORT = new Errno(TYPE, 153, "Non-regular file found at %1$s path.");
|
||||
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND = new Errno(TYPE, 154, "Non-directory file found at %1$s path \"%2$s\".");
|
||||
public static final Errno ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT = new Errno(TYPE, 155, "Non-directory file found at %1$s path.");
|
||||
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND = new Errno(TYPE, 156, "Non-symlink file found at %1$s path \"%2$s\".");
|
||||
public static final Errno ERRNO_NON_SYMLINK_FILE_FOUND_SHORT = new Errno(TYPE, 157, "Non-symlink file found at %1$s path.");
|
||||
|
||||
public static final Errno ERRNO_FILE_NOT_AN_ALLOWED_FILE_TYPE = new Errno(TYPE, 158, "The %1$s found at path \"%2$s\" is not one of allowed file types \"%3$s\".");
|
||||
|
||||
public static final Errno ERRNO_VALIDATE_FILE_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 159, "Validating file existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_VALIDATE_DIRECTORY_EXISTENCE_AND_PERMISSIONS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 160, "Validating directory existence and permissions of %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for file creation (200-250) */
|
||||
public static final Errno ERRNO_CREATING_FILE_FAILED = new Errno(TYPE, 200, "Creating %1$s at path \"%2$s\" failed.");
|
||||
public static final Errno ERRNO_CREATING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 201, "Creating %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
|
||||
public static final Errno ERRNO_CANNOT_OVERWRITE_A_NON_SYMLINK_FILE_TYPE = new Errno(TYPE, 202, "Cannot overwrite %1$s while creating symlink at \"%2$s\" to \"%3$s\" since destination file type \"%4$s\" is not a symlink.");
|
||||
public static final Errno ERRNO_CREATING_SYMLINK_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 203, "Creating %1$s at path \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for file copying and moving (250-300) */
|
||||
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 250, "%1$s from \"%2$s\" to \"%3$s\" failed.\nException: %4$s");
|
||||
public static final Errno ERRNO_COPYING_OR_MOVING_FILE_TO_SAME_PATH = new Errno(TYPE, 251, "%1$s from \"%2$s\" to \"%3$s\" cannot be done since they point to the same path.");
|
||||
public static final Errno ERRNO_CANNOT_OVERWRITE_A_DIFFERENT_FILE_TYPE = new Errno(TYPE, 252, "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\".");
|
||||
public static final Errno ERRNO_CANNOT_MOVE_DIRECTORY_TO_SUB_DIRECTORY_OF_ITSELF = new Errno(TYPE, 253, "Cannot move %1$s from \"%2$s\" to \"%3$s\" since destination is a subdirectory of the source.");
|
||||
|
||||
|
||||
|
||||
/* Errors for file deletion (300-350) */
|
||||
public static final Errno ERRNO_DELETING_FILE_FAILED = new Errno(TYPE, 300, "Deleting %1$s at path \"%2$s\" failed.");
|
||||
public static final Errno ERRNO_DELETING_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 301, "Deleting %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_CLEARING_DIRECTORY_FAILED_WITH_EXCEPTION = new Errno(TYPE, 302, "Clearing %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_FILE_STILL_EXISTS_AFTER_DELETING = new Errno(TYPE, 303, "The %1$s still exists after deleting it from \"%2$s\".");
|
||||
public static final Errno ERRNO_DELETING_FILES_OLDER_THAN_X_DAYS_FAILED_WITH_EXCEPTION = new Errno(TYPE, 304, "Deleting %1$s under directory at path \"%2$s\" old than %3$s days failed.\nException: %4$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for file reading and writing (350-400) */
|
||||
public static final Errno ERRNO_READING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 350, "Reading string from %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_WRITING_STRING_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 351, "Writing string to %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_UNSUPPORTED_CHARSET = new Errno(TYPE, 352, "Unsupported charset \"%1$s\"");
|
||||
public static final Errno ERRNO_CHECKING_IF_CHARSET_SUPPORTED_FAILED = new Errno(TYPE, 353, "Checking if charset \"%1$s\" is supported failed.\nException: %2$s");
|
||||
public static final Errno ERRNO_READING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 354, "Reading serializable object from %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
public static final Errno ERRNO_WRITING_SERIALIZABLE_OBJECT_TO_FILE_FAILED_WITH_EXCEPTION = new Errno(TYPE, 355, "Writing serializable object to %1$s at path \"%2$s\" failed.\nException: %3$s");
|
||||
|
||||
|
||||
|
||||
/* Errors for invalid file permissions (400-450) */
|
||||
public static final Errno ERRNO_INVALID_FILE_PERMISSIONS_STRING_TO_CHECK = new Errno(TYPE, 400, "The file permission string to check is invalid.");
|
||||
public static final Errno ERRNO_FILE_NOT_READABLE = new Errno(TYPE, 401, "The %1$s at path \"%2$s\" is not readable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_READABLE_SHORT = new Errno(TYPE, 402, "The %1$s at path is not readable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_WRITABLE = new Errno(TYPE, 403, "The %1$s at path \"%2$s\" is not writable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_WRITABLE_SHORT = new Errno(TYPE, 404, "The %1$s at path is not writable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_EXECUTABLE = new Errno(TYPE, 405, "The %1$s at path \"%2$s\" is not executable. Permission Denied.");
|
||||
public static final Errno ERRNO_FILE_NOT_EXECUTABLE_SHORT = new Errno(TYPE, 406, "The %1$s at path is not executable. Permission Denied.");
|
||||
|
||||
|
||||
FileUtilsErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Defines the {@link Errno} mapping to get a shorter version of {@link FileUtilsErrno}. */
|
||||
public static Map<Errno, Errno> ERRNO_SHORT_MAPPING = new HashMap<Errno, Errno>() {{
|
||||
put(ERRNO_FILE_NOT_FOUND_AT_PATH, ERRNO_FILE_NOT_FOUND_AT_PATH_SHORT);
|
||||
|
||||
put(ERRNO_NON_REGULAR_FILE_FOUND, ERRNO_NON_REGULAR_FILE_FOUND_SHORT);
|
||||
put(ERRNO_NON_DIRECTORY_FILE_FOUND, ERRNO_NON_DIRECTORY_FILE_FOUND_SHORT);
|
||||
put(ERRNO_NON_SYMLINK_FILE_FOUND, ERRNO_NON_SYMLINK_FILE_FOUND_SHORT);
|
||||
|
||||
put(ERRNO_FILE_NOT_READABLE, ERRNO_FILE_NOT_READABLE_SHORT);
|
||||
put(ERRNO_FILE_NOT_WRITABLE, ERRNO_FILE_NOT_WRITABLE_SHORT);
|
||||
put(ERRNO_FILE_NOT_EXECUTABLE, ERRNO_FILE_NOT_EXECUTABLE_SHORT);
|
||||
}};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package com.termux.shared.models.errors;
|
||||
|
||||
/** The {@link Class} that defines function error messages and codes. */
|
||||
public class FunctionErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "Function Error";
|
||||
|
||||
|
||||
/* Errors for null or empty parameters (100-150) */
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETER = new Errno(TYPE, 100, "The %1$s parameter passed to \"%2$s\" is null or empty.");
|
||||
public static final Errno ERRNO_NULL_OR_EMPTY_PARAMETERS = new Errno(TYPE, 101, "The %1$s parameters passed to \"%2$s\" are null or empty.");
|
||||
public static final Errno ERRNO_UNSET_PARAMETER = new Errno(TYPE, 102, "The %1$s parameter passed to \"%2$s\" must be set.");
|
||||
public static final Errno ERRNO_UNSET_PARAMETERS = new Errno(TYPE, 103, "The %1$s parameters passed to \"%2$s\" must be set.");
|
||||
public static final Errno ERRNO_INVALID_PARAMETER = new Errno(TYPE, 104, "The %1$s parameter passed to \"%2$s\" is invalid.\"%3$s\"");
|
||||
|
||||
|
||||
FunctionErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.termux.shared.models.errors;
|
||||
|
||||
/** The {@link Class} that defines ResultSender error messages and codes. */
|
||||
public class ResultSenderErrno extends Errno {
|
||||
|
||||
public static final String TYPE = "ResultSender Error";
|
||||
|
||||
|
||||
/* Errors for null or empty parameters (100-150) */
|
||||
public static final Errno ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID = new Errno(TYPE, 100, "The result file basename \"%1$s\" is null, empty or contains forward slashes \"/\".");
|
||||
public static final Errno ERROR_RESULT_FILES_SUFFIX_INVALID = new Errno(TYPE, 101, "The result files suffix \"%1$s\" contains forward slashes \"/\".");
|
||||
public static final Errno ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION = new Errno(TYPE, 102, "Formatting result error failed.\nException: %1$s");
|
||||
public static final Errno ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION = new Errno(TYPE, 103, "Formatting result output failed.\nException: %1$s");
|
||||
|
||||
|
||||
ResultSenderErrno(final String type, final int code, final String message) {
|
||||
super(type, code, message);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get {@link Notification.Builder}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param title The title for the notification.
|
||||
* @param notificationText The second line text of the notification.
|
||||
* @param notificationBigText The full text of the notification that may optionally be styled.
|
||||
* @param contentIntent The {@link PendingIntent} which should be sent when notification is clicked.
|
||||
* @param deleteIntent The {@link PendingIntent} which should be sent when notification is deleted.
|
||||
* @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 notificationText, final CharSequence notificationBigText,
|
||||
final PendingIntent contentIntent, final PendingIntent deleteIntent, final int notificationMode) {
|
||||
if (context == null) return null;
|
||||
Notification.Builder builder = new Notification.Builder(context);
|
||||
builder.setContentTitle(title);
|
||||
builder.setContentText(notificationText);
|
||||
builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText));
|
||||
builder.setContentIntent(contentIntent);
|
||||
builder.setDeleteIntent(deleteIntent);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
package com.termux.shared.notification;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.termux.shared.settings.preferences.TermuxAppSharedPreferences;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxNotificationUtils {
|
||||
/**
|
||||
* 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 = TermuxAppSharedPreferences.build(context);
|
||||
if (preferences == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,471 @@
|
|||
package com.termux.shared.packages;
|
||||
|
||||
import android.app.ActivityManager;
|
||||
import android.app.admin.DevicePolicyManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.UserManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.interact.MessageDialogUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.reflection.ReflectionUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.List;
|
||||
|
||||
public class PackageUtils {
|
||||
|
||||
private static final String LOG_TAG = "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}.
|
||||
* @param packageName The package name whose {@link Context} to get.
|
||||
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Context getContextForPackage(@NonNull final Context context, String packageName) {
|
||||
try {
|
||||
return context.createPackageContext(packageName, Context.CONTEXT_RESTRICTED);
|
||||
} catch (Exception e) {
|
||||
Logger.logVerbose(LOG_TAG, "Failed to get \"" + packageName + "\" package context: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the {@code packageName}.
|
||||
* @param packageName The package name whose {@link Context} to get.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link Context}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static Context getContextForPackageOrExitApp(@NonNull Context context, String packageName, final boolean exitAppOnError) {
|
||||
Context packageContext = getContextForPackage(context, packageName);
|
||||
|
||||
if (packageContext == null && exitAppOnError) {
|
||||
String errorMessage = context.getString(R.string.error_get_package_context_failed_message,
|
||||
packageName, TermuxConstants.TERMUX_GITHUB_REPO_URL);
|
||||
Logger.logError(LOG_TAG, errorMessage);
|
||||
MessageDialogUtils.exitAppWithErrorMessage(context,
|
||||
context.getString(R.string.error_get_package_context_failed_title),
|
||||
errorMessage);
|
||||
}
|
||||
|
||||
return packageContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return getPackageInfoForPackage(context, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link PackageInfo} for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @param flags The flags to pass to {@link PackageManager#getPackageInfo(String, int)}.
|
||||
* @return Returns the {@link PackageInfo}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static PackageInfo getPackageInfoForPackage(@NonNull final Context context, final int flags) {
|
||||
try {
|
||||
return context.getPackageManager().getPackageInfo(context.getPackageName(), flags);
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfo} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* String retrieved from the seinfo tag found in selinux policy. This value can be set through
|
||||
* the mac_permissions.xml policy construct. This value is used for setting an SELinux security
|
||||
* context on the process as well as its data directory.
|
||||
*
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.1.0_r1:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=609
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/core/java/android/content/pm/ApplicationInfo.java;l=981
|
||||
* https://cs.android.com/android/platform/superproject/+/android-7.0.0_r1:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=282
|
||||
* https://cs.android.com/android/platform/superproject/+/android-12.0.0_r32:frameworks/base/services/core/java/com/android/server/pm/SELinuxMMAC.java;l=375
|
||||
* https://cs.android.com/android/_/android/platform/frameworks/base/+/be0b8896d1bc385d4c8fb54c21929745935dcbea
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? "seinfo" : "seInfo", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfo field value for ApplicationInfo class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code seInfoUser} {@link Field} of the {@link ApplicationInfo} class.
|
||||
*
|
||||
* Also check {@link #getApplicationInfoSeInfoForPackage(ApplicationInfo)}.
|
||||
*
|
||||
* @param applicationInfo The {@link ApplicationInfo} for the package.
|
||||
* @return Returns the selinux info user or {@code null} if an exception was raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getApplicationInfoSeInfoUserForPackage(@NonNull final ApplicationInfo applicationInfo) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null;
|
||||
ReflectionUtils.bypassHiddenAPIReflectionRestrictions();
|
||||
try {
|
||||
return (String) ReflectionUtils.invokeField(ApplicationInfo.class, "seInfoUser", applicationInfo).value;
|
||||
} catch (Exception e) {
|
||||
// ClassCastException may be thrown
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get seInfoUser field value for ApplicationInfo class", 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_DEBUGGABLE}
|
||||
* set.
|
||||
*
|
||||
* @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 isAppForPackageADebuggableBuild(@NonNull final Context context) {
|
||||
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the app associated with the {@code context} has {@link ApplicationInfo#FLAG_EXTERNAL_STORAGE}
|
||||
* set.
|
||||
*
|
||||
* @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 isAppInstalledOnExternalStorage(@NonNull final Context context) {
|
||||
return ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_EXTERNAL_STORAGE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
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.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getVersionNameForPackage(@NonNull final Context context) {
|
||||
try {
|
||||
return getPackageInfoForPackage(context).versionName;
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code SHA-256 digest} of signing certificate for the package associated with the {@code context}.
|
||||
*
|
||||
* @param context The {@link Context} for the package.
|
||||
* @return Returns the {@code SHA-256 digest}. This will be {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getSigningCertificateSHA256DigestForPackage(@NonNull final Context context) {
|
||||
try {
|
||||
/*
|
||||
* Todo: We may need AndroidManifest queries entries if package is installed but with a different signature on android 11
|
||||
* https://developer.android.com/training/package-visibility
|
||||
* Need a device that allows (manual) installation of apk with mismatched signature of
|
||||
* sharedUserId apps to test. Currently, if its done, PackageManager just doesn't load
|
||||
* the package and removes its apk automatically if its installed as a user app instead of system app
|
||||
* W/PackageManager: Failed to parse /path/to/com.termux.tasker.apk: Signature mismatch for shared user: SharedUserSetting{xxxxxxx com.termux/10xxx}
|
||||
*/
|
||||
PackageInfo packageInfo = getPackageInfoForPackage(context, PackageManager.GET_SIGNATURES);
|
||||
if (packageInfo == null) return null;
|
||||
return DataUtils.bytesToHex(MessageDigest.getInstance("SHA-256").digest(packageInfo.signatures[0].toByteArray()));
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the serial number for the current user.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns the serial number. This will be {@code null} if failed to get it.
|
||||
*/
|
||||
@Nullable
|
||||
public static Long getSerialNumberForCurrentUser(@NonNull Context context) {
|
||||
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
|
||||
if (userManager == null) return null;
|
||||
return userManager.getSerialNumberForUser(android.os.Process.myUserHandle());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is the primary user. This is done by checking if the the serial
|
||||
* number for the current user equals 0.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns {@code true} if the current user is the primary user, otherwise [@code false}.
|
||||
*/
|
||||
public static boolean isCurrentUserThePrimaryUser(@NonNull Context context) {
|
||||
Long userId = getSerialNumberForCurrentUser(context);
|
||||
return userId != null && userId == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the profile owner package name for the current user.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @return Returns the profile owner package name. This will be {@code null} if failed to get it
|
||||
* or no profile owner for the current user.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getProfileOwnerPackageNameForUser(@NonNull Context context) {
|
||||
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
|
||||
if (devicePolicyManager == null) return null;
|
||||
List<ComponentName> activeAdmins = devicePolicyManager.getActiveAdmins();
|
||||
if (activeAdmins != null){
|
||||
for (ComponentName admin:activeAdmins){
|
||||
String packageName = admin.getPackageName();
|
||||
if(devicePolicyManager.isProfileOwnerApp(packageName))
|
||||
return packageName;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the process id of the main app process of a package. This will work for sharedUserId. Note
|
||||
* that some apps have multiple processes for the app like with `android:process=":background"`
|
||||
* attribute in AndroidManifest.xml.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the process.
|
||||
* @return Returns the process if found and running, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getPackagePID(final Context context, String packageName) {
|
||||
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
|
||||
if (activityManager != null) {
|
||||
List<ActivityManager.RunningAppProcessInfo> processInfos = activityManager.getRunningAppProcesses();
|
||||
if (processInfos != null) {
|
||||
ActivityManager.RunningAppProcessInfo processInfo;
|
||||
for (int i = 0; i < processInfos.size(); i++) {
|
||||
processInfo = processInfos.get(i);
|
||||
if (processInfo.processName.equals(packageName))
|
||||
return String.valueOf(processInfo.pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if app is installed and enabled. This can be used by external apps that don't
|
||||
* share `sharedUserId` with the an app.
|
||||
*
|
||||
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add package
|
||||
* name to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
||||
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... package_name/......} BLOCKED`
|
||||
* errors in `logcat` and `RUN_COMMAND` won't work.
|
||||
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
|
||||
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
|
||||
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
|
||||
*
|
||||
* {@code
|
||||
* <manifest
|
||||
* <queries>
|
||||
* <package android:name="package_name" />
|
||||
* </queries>
|
||||
* </manifest>
|
||||
* }
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns {@code errmsg} if {@code packageName} is not installed or disabled, otherwise {@code null}.
|
||||
*/
|
||||
public static String isAppInstalled(@NonNull final Context context, String appName, String packageName) {
|
||||
String errmsg = null;
|
||||
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
|
||||
ApplicationInfo applicationInfo;
|
||||
try {
|
||||
applicationInfo = packageManager.getApplicationInfo(packageName, 0);
|
||||
} catch (final PackageManager.NameNotFoundException e) {
|
||||
applicationInfo = null;
|
||||
}
|
||||
boolean isAppEnabled = (applicationInfo != null && applicationInfo.enabled);
|
||||
|
||||
// If app is not installed or is disabled
|
||||
if (!isAppEnabled)
|
||||
errmsg = context.getString(R.string.error_app_not_installed_or_disabled_warning, appName, packageName);
|
||||
|
||||
return errmsg;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Enable or disable a {@link ComponentName} with a call to
|
||||
* {@link PackageManager#setComponentEnabledSetting(ComponentName, int, int)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param state If component should be enabled or disabled.
|
||||
* @param toastString If this is not {@code null} or empty, then a toast before setting state.
|
||||
* @param showErrorMessage If an error message toast should be shown.
|
||||
* @return Returns the errmsg if failed to set state, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static String setComponentState(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, boolean state, String toastString,
|
||||
boolean showErrorMessage) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
ComponentName componentName = new ComponentName(packageName, className);
|
||||
if (toastString != null) Logger.showToast(context, toastString, true);
|
||||
packageManager.setComponentEnabledSetting(componentName,
|
||||
state ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
PackageManager.DONT_KILL_APP);
|
||||
}
|
||||
return null;
|
||||
} catch (final Exception e) {
|
||||
String errmsg = context.getString(
|
||||
state ? R.string.error_enable_component_failed : R.string.error_disable_component_failed,
|
||||
packageName, className) + ": " + e.getMessage();
|
||||
if (showErrorMessage)
|
||||
Logger.showToast(context, errmsg, true);
|
||||
return errmsg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if state of a {@link ComponentName} is {@link PackageManager#COMPONENT_ENABLED_STATE_DISABLED}
|
||||
* with a call to {@link PackageManager#getComponentEnabledSetting(ComponentName)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param logErrorMessage If an error message should be logged.
|
||||
* @return Returns {@code true} if disabled, {@code false} if not and {@code null} if failed to
|
||||
* get the state.
|
||||
*/
|
||||
public static Boolean isComponentDisabled(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, boolean logErrorMessage) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
ComponentName componentName = new ComponentName(packageName, className);
|
||||
// Will throw IllegalArgumentException: Unknown component: ComponentInfo{} if app
|
||||
// for context is not installed or component does not exist.
|
||||
return packageManager.getComponentEnabledSetting(componentName) == PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
if (logErrorMessage)
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, context.getString(R.string.error_get_component_state_failed, packageName, className), e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an {@link android.app.Activity} {@link ComponentName} can be called by calling
|
||||
* {@link PackageManager#queryIntentActivities(Intent, int)}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param packageName The package name of the component.
|
||||
* @param className The {@link Class} name of the component.
|
||||
* @param flags The flags to filter results.
|
||||
* @return Returns {@code true} if it exists, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean doesActivityComponentExist(@NonNull final Context context, @NonNull String packageName,
|
||||
@NonNull String className, int flags) {
|
||||
try {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
if (packageManager != null) {
|
||||
Intent intent = new Intent();
|
||||
intent.setClassName(packageName, className);
|
||||
return packageManager.queryIntentActivities(intent, flags).size() > 0;
|
||||
}
|
||||
} catch (final Exception e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
package com.termux.shared.packages;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
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.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class PermissionUtils {
|
||||
|
||||
public static final int REQUEST_GRANT_STORAGE_PERMISSION = 1000;
|
||||
|
||||
public static final int REQUEST_DISABLE_BATTERY_OPTIMIZATIONS = 2000;
|
||||
public static final int REQUEST_GRANT_DISPLAY_OVER_OTHER_APPS_PERMISSION = 2001;
|
||||
|
||||
private static final String LOG_TAG = "PermissionUtils";
|
||||
|
||||
|
||||
public static boolean checkPermission(Context context, String permission) {
|
||||
if (permission == null) return false;
|
||||
return checkPermissions(context, new String[]{permission});
|
||||
}
|
||||
|
||||
public static boolean checkPermissions(Context context, String[] permissions) {
|
||||
if (permissions == null) return false;
|
||||
|
||||
int result;
|
||||
for (String p:permissions) {
|
||||
result = ContextCompat.checkSelfPermission(context,p);
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static void requestPermission(Activity activity, String permission, int requestCode) {
|
||||
if (permission == null) return;
|
||||
requestPermissions(activity, new String[]{permission}, requestCode);
|
||||
}
|
||||
|
||||
public static void requestPermissions(Activity activity, String[] permissions, int requestCode) {
|
||||
if (activity == null || permissions == null) return;
|
||||
|
||||
int result;
|
||||
Logger.showToast(activity, activity.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(activity, permission);
|
||||
if (result != PackageManager.PERMISSION_GRANTED) {
|
||||
Logger.logDebug(LOG_TAG, "Requesting Permissions: " + Arrays.toString(permissions));
|
||||
try {
|
||||
activity.requestPermissions(new String[]{permission}, requestCode);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to request permissions with request code " + requestCode + ": " + Arrays.toString(permissions), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean checkDisplayOverOtherAppsPermission(Context context) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
}
|
||||
|
||||
public static void requestDisplayOverOtherAppsPermission(Activity context, int requestCode) {
|
||||
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
|
||||
context.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
public static boolean validateDisplayOverOtherAppsPermissionForPostAndroid10(Context context, boolean logResults) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) return true;
|
||||
|
||||
if (!PermissionUtils.checkDisplayOverOtherAppsPermission(context)) {
|
||||
if (logResults)
|
||||
Logger.logWarn(LOG_TAG, context.getPackageName() + " does not have Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return false;
|
||||
} else {
|
||||
if (logResults)
|
||||
Logger.logDebug(LOG_TAG, context.getPackageName() + " already has Display over other apps (SYSTEM_ALERT_WINDOW) permission");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static boolean checkIfBatteryOptimizationsDisabled(Context context) {
|
||||
PowerManager powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return powerManager.isIgnoringBatteryOptimizations(context.getPackageName());
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
public static void requestDisableBatteryOptimizations(Activity activity, int requestCode) {
|
||||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + activity.getPackageName()));
|
||||
activity.startActivityForResult(intent, requestCode);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,275 @@
|
|||
package com.termux.shared.reflection;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
import org.lsposed.hiddenapibypass.HiddenApiBypass;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ReflectionUtils {
|
||||
|
||||
private static boolean HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = Build.VERSION.SDK_INT < Build.VERSION_CODES.P;
|
||||
|
||||
private static final String LOG_TAG = "ReflectionUtils";
|
||||
|
||||
/**
|
||||
* Bypass android hidden API reflection restrictions.
|
||||
* https://github.com/LSPosed/AndroidHiddenApiBypass
|
||||
* https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces
|
||||
*/
|
||||
public static void bypassHiddenAPIReflectionRestrictions() {
|
||||
if (!HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
Logger.logDebug(LOG_TAG, "Bypassing android hidden api reflection restrictions");
|
||||
HiddenApiBypass.addHiddenApiExemptions("");
|
||||
HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED = true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Check if android hidden API reflection restrictions are bypassed. */
|
||||
public static boolean areHiddenAPIReflectionRestrictionsBypassed() {
|
||||
return HIDDEN_API_REFLECTION_RESTRICTIONS_BYPASSED;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get a {@link Field} for the specified class.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the field.
|
||||
* @param fieldName The name of the {@link Field}.
|
||||
* @return Returns the {@link Field} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Field getDeclaredField(@NonNull Class<?> clazz, @NonNull String fieldName) {
|
||||
try {
|
||||
Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field for \"" + clazz.getName() + "\" class", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Class that represents result of invoking a field. */
|
||||
public static class FieldInvokeResult {
|
||||
public boolean success;
|
||||
public Object value;
|
||||
|
||||
FieldInvokeResult(boolean success, Object value) {
|
||||
this.value = success;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value for a {@link Field} of an object for the specified class.
|
||||
*
|
||||
* @param clazz The {@link Class} to which the object belongs to.
|
||||
* @param fieldName The name of the {@link Field}.
|
||||
* @param object The {@link Object} instance from which to get the field value.
|
||||
* @return Returns the {@link FieldInvokeResult} of invoking the field. The
|
||||
* {@link FieldInvokeResult#success} will be {@code true} if invoking the field was successful,
|
||||
* otherwise {@code false}. The {@link FieldInvokeResult#value} will contain the field
|
||||
* {@link Object} value.
|
||||
*/
|
||||
@NonNull
|
||||
public static <T> FieldInvokeResult invokeField(@NonNull Class<T> clazz, @NonNull String fieldName, T object) {
|
||||
try {
|
||||
Field field = getDeclaredField(clazz, fieldName);
|
||||
if (field == null) return new FieldInvokeResult(false, null);
|
||||
return new FieldInvokeResult(true, field.get(object));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + fieldName + "\" field value for \"" + clazz.getName() + "\" class", e);
|
||||
return new FieldInvokeResult(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getDeclaredMethod(Class, String, Class[])} without parameters.
|
||||
*/
|
||||
@Nullable
|
||||
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName) {
|
||||
return getDeclaredMethod(clazz, methodName, new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link Method} for the specified class with the specified parameters.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the method.
|
||||
* @param methodName The name of the {@link Method}.
|
||||
* @param parameterTypes The parameter types of the method.
|
||||
* @return Returns the {@link Method} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Method getDeclaredMethod(@NonNull Class<?> clazz, @NonNull String methodName, Class<?>... parameterTypes) {
|
||||
try {
|
||||
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get \"" + methodName + "\" method for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeVoidMethod(Method, Object, Object...)} without arguments.
|
||||
*/
|
||||
public static boolean invokeVoidMethod(@NonNull Method method, Object obj) {
|
||||
return invokeVoidMethod(method, obj, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Method} on the specified object with the specified arguments that returns
|
||||
* {@code void}.
|
||||
*
|
||||
* @param method The {@link Method} to invoke.
|
||||
* @param obj The {@link Object} the method should be invoked from.
|
||||
* @param args The arguments to pass to the method.
|
||||
* @return Returns {@code true} if invoking the method was successful, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean invokeVoidMethod(@NonNull Method method, Object obj, Object... args) {
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
method.invoke(obj, args);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Class that represents result of invoking a method that has a non-void return type. */
|
||||
public static class MethodInvokeResult {
|
||||
public boolean success;
|
||||
public Object value;
|
||||
|
||||
MethodInvokeResult(boolean success, Object value) {
|
||||
this.value = success;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeMethod(Method, Object, Object...)} without arguments.
|
||||
*/
|
||||
@NonNull
|
||||
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj) {
|
||||
return invokeMethod(method, obj, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Method} on the specified object with the specified arguments.
|
||||
*
|
||||
* @param method The {@link Method} to invoke.
|
||||
* @param obj The {@link Object} the method should be invoked from.
|
||||
* @param args The arguments to pass to the method.
|
||||
* @return Returns the {@link MethodInvokeResult} of invoking the method. The
|
||||
* {@link MethodInvokeResult#success} will be {@code true} if invoking the method was successful,
|
||||
* otherwise {@code false}. The {@link MethodInvokeResult#value} will contain the {@link Object}
|
||||
* returned by the method.
|
||||
*/
|
||||
@NonNull
|
||||
public static MethodInvokeResult invokeMethod(@NonNull Method method, Object obj, Object... args) {
|
||||
try {
|
||||
method.setAccessible(true);
|
||||
return new MethodInvokeResult(true, method.invoke(obj, args));
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + method.getName() + "\" method with object \"" + obj + "\" and args: " + Arrays.toString(args), e);
|
||||
return new MethodInvokeResult(false, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getConstructor(String, Class[])} without parameters.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull String className) {
|
||||
return getConstructor(className, new Class<?>[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #getConstructor(Class, Class[])} to get a {@link Constructor} for the
|
||||
* {@code className}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull String className, Class<?>... parameterTypes) {
|
||||
try {
|
||||
return getConstructor(Class.forName(className), parameterTypes);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + className + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a {@link Constructor} for the specified class with the specified parameters.
|
||||
*
|
||||
* @param clazz The {@link Class} for which to return the constructor.
|
||||
* @param parameterTypes The parameter types of the constructor.
|
||||
* @return Returns the {@link Constructor} if getting the it was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Constructor<?> getConstructor(@NonNull Class<?> clazz, Class<?>... parameterTypes) {
|
||||
try {
|
||||
Constructor<?> constructor = clazz.getConstructor(parameterTypes);
|
||||
constructor.setAccessible(true);
|
||||
return constructor;
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to get constructor for \"" + clazz.getName() + "\" class with parameter types: " + Arrays.toString(parameterTypes), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for {@link #invokeConstructor(Constructor, Object...)} without arguments.
|
||||
*/
|
||||
@Nullable
|
||||
public static Object invokeConstructor(@NonNull Constructor<?> constructor) {
|
||||
return invokeConstructor(constructor, new Object[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a {@link Constructor} with the specified arguments.
|
||||
*
|
||||
* @param constructor The {@link Constructor} to invoke.
|
||||
* @param args The arguments to pass to the constructor.
|
||||
* @return Returns the new instance if invoking the constructor was successful, otherwise {@code null}.
|
||||
*/
|
||||
@Nullable
|
||||
public static Object invokeConstructor(@NonNull Constructor<?> constructor, Object... args) {
|
||||
try {
|
||||
constructor.setAccessible(true);
|
||||
return constructor.newInstance(args);
|
||||
} catch (Exception e) {
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to invoke \"" + constructor.getName() + "\" constructor with args: " + Arrays.toString(args), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
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.
|
||||
* @param defIfEmpty If set to {@code true}, then {@code def} will be returned if value is empty.
|
||||
* @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, boolean defIfEmpty) {
|
||||
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 {
|
||||
String value = sharedPreferences.getString(key, def);
|
||||
if (defIfEmpty && (value == null || value.isEmpty()))
|
||||
return def;
|
||||
else
|
||||
return value;
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxAPIAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TermuxAPIAppSharedPreferences";
|
||||
|
||||
private TermuxAPIAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_API_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxAPIAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxAPIAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_API_PACKAGE_NAME);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxAPIAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_API_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxAPIAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxAPIAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_API_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxAPIAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_API_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
|
||||
else
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_API_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_API_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_APP;
|
||||
|
||||
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";
|
||||
|
||||
private TermuxAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
|
||||
setFontVariables(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_PACKAGE_NAME);
|
||||
if (termuxPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxAppSharedPreferences(termuxPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxAppSharedPreferences(termuxPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean shouldShowTerminalToolbar() {
|
||||
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 = shouldShowTerminalToolbar();
|
||||
setShowTerminalToolbar(!currentValue);
|
||||
return !currentValue;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean isTerminalMarginAdjustmentEnabled() {
|
||||
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_MARGIN_ADJUSTMENT, TERMUX_APP.DEFAULT_TERMINAL_MARGIN_ADJUSTMENT);
|
||||
}
|
||||
|
||||
public void setTerminalMarginAdjustment(boolean value) {
|
||||
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_MARGIN_ADJUSTMENT, value, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean isSoftKeyboardEnabled() {
|
||||
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 isSoftKeyboardEnabledOnlyIfNoHardware() {
|
||||
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE, TERMUX_APP.DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE);
|
||||
}
|
||||
|
||||
public void setSoftKeyboardEnabledOnlyIfNoHardware(boolean value) {
|
||||
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_APP.KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE, value, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public boolean shouldKeepScreenOn() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static int[] getDefaultFontSizes(Context context) {
|
||||
float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
|
||||
|
||||
int[] sizes = new int[3];
|
||||
|
||||
// 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:
|
||||
sizes[1] = (int) (4f * dipInPixels); // min
|
||||
|
||||
// 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--;
|
||||
|
||||
sizes[0] = defaultFontSize; // default
|
||||
|
||||
sizes[2] = 256; // max
|
||||
|
||||
return sizes;
|
||||
}
|
||||
|
||||
public void setFontVariables(Context context) {
|
||||
int[] sizes = getDefaultFontSizes(context);
|
||||
|
||||
DEFAULT_FONTSIZE = sizes[0];
|
||||
MIN_FONTSIZE = sizes[1];
|
||||
MAX_FONTSIZE = sizes[2];
|
||||
}
|
||||
|
||||
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, true);
|
||||
}
|
||||
|
||||
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 isTerminalViewKeyLoggingEnabled() {
|
||||
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 arePluginErrorNotificationsEnabled() {
|
||||
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 areCrashReportNotificationsEnabled() {
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_BOOT_APP;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxBootAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TermuxBootAppSharedPreferences";
|
||||
|
||||
private TermuxBootAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_BOOT_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxBootAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxBootAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_BOOT_PACKAGE_NAME);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxBootAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_BOOT_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxBootAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxBootAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_BOOT_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxBootAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_BOOT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_BOOT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
|
||||
else
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_BOOT_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_BOOT_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_FLOAT_APP;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxFloatAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
private int MIN_FONTSIZE;
|
||||
private int MAX_FONTSIZE;
|
||||
private int DEFAULT_FONTSIZE;
|
||||
|
||||
private static final String LOG_TAG = "TermuxFloatAppSharedPreferences";
|
||||
|
||||
private TermuxFloatAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
|
||||
setFontVariables(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_FLOAT_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxFloatAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxFloatAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxFloatPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME);
|
||||
if (termuxFloatPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxFloatAppSharedPreferences(termuxFloatPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_FLOAT_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxFloatAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxFloatAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxFloatPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_FLOAT_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxFloatPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxFloatAppSharedPreferences(termuxFloatPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_FLOAT_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getWindowX() {
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_X, 200);
|
||||
|
||||
}
|
||||
|
||||
public void setWindowX(int value) {
|
||||
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_X, value, false);
|
||||
}
|
||||
|
||||
public int getWindowY() {
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_Y, 200);
|
||||
|
||||
}
|
||||
|
||||
public void setWindowY(int value) {
|
||||
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_Y, value, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getWindowWidth() {
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_WIDTH, 500);
|
||||
|
||||
}
|
||||
|
||||
public void setWindowWidth(int value) {
|
||||
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_WIDTH, value, false);
|
||||
}
|
||||
|
||||
public int getWindowHeight() {
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_HEIGHT, 500);
|
||||
|
||||
}
|
||||
|
||||
public void setWindowHeight(int value) {
|
||||
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_FLOAT_APP.KEY_WINDOW_HEIGHT, value, false);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void setFontVariables(Context context) {
|
||||
int[] sizes = TermuxAppSharedPreferences.getDefaultFontSizes(context);
|
||||
|
||||
DEFAULT_FONTSIZE = sizes[0];
|
||||
MIN_FONTSIZE = sizes[1];
|
||||
MAX_FONTSIZE = sizes[2];
|
||||
}
|
||||
|
||||
public int getFontSize() {
|
||||
int fontSize = SharedPreferenceUtils.getIntStoredAsString(mSharedPreferences, TERMUX_FLOAT_APP.KEY_FONTSIZE, DEFAULT_FONTSIZE);
|
||||
return DataUtils.clamp(fontSize, MIN_FONTSIZE, MAX_FONTSIZE);
|
||||
}
|
||||
|
||||
public void setFontSize(int value) {
|
||||
SharedPreferenceUtils.setIntStoredAsString(mSharedPreferences, TERMUX_FLOAT_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 int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_FLOAT_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
|
||||
else
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_FLOAT_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_FLOAT_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
|
||||
}
|
||||
|
||||
|
||||
public boolean isTerminalViewKeyLoggingEnabled(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getBoolean(mMultiProcessSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_FLOAT_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
|
||||
else
|
||||
return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_FLOAT_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED);
|
||||
}
|
||||
|
||||
public void setTerminalViewKeyLoggingEnabled(boolean value, boolean commitToFile) {
|
||||
SharedPreferenceUtils.setBoolean(mSharedPreferences, TERMUX_FLOAT_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, value, commitToFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,294 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
/*
|
||||
* Version: v0.15.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.
|
||||
*
|
||||
* - 0.10.0 (2021-05-12)
|
||||
* - Added following to `TERMUX_APP`:
|
||||
* `KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE` and `DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE`.
|
||||
*
|
||||
* - 0.11.0 (2021-07-08)
|
||||
* - Added following to `TERMUX_APP`:
|
||||
* `KEY_DISABLE_TERMINAL_MARGIN_ADJUSTMENT`.
|
||||
*
|
||||
* - 0.12.0 (2021-08-27)
|
||||
* - Added `TERMUX_API_APP.KEY_LOG_LEVEL`, `TERMUX_BOOT_APP.KEY_LOG_LEVEL`,
|
||||
* `TERMUX_FLOAT_APP.KEY_LOG_LEVEL`, `TERMUX_STYLING_APP.KEY_LOG_LEVEL`,
|
||||
* `TERMUX_Widget_APP.KEY_LOG_LEVEL`.
|
||||
*
|
||||
* - 0.13.0 (2021-09-02)
|
||||
* - Added following to `TERMUX_FLOAT_APP`:
|
||||
* `KEY_WINDOW_X`, `KEY_WINDOW_Y`, `KEY_WINDOW_WIDTH`, `KEY_WINDOW_HEIGHT`, `KEY_FONTSIZE`,
|
||||
* `KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED`.
|
||||
*
|
||||
* - 0.14.0 (2021-09-04)
|
||||
* - Added `TERMUX_WIDGET_APP.KEY_TOKEN`.
|
||||
*
|
||||
* - 0.15.0 (2021-09-05)
|
||||
* - Added following to `TERMUX_TASKER_APP`:
|
||||
* `KEY_LAST_PENDING_INTENT_REQUEST_CODE` and `DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 terminal view margin adjustment that is done to prevent soft
|
||||
* keyboard from covering bottom part of terminal view on some devices is enabled or not.
|
||||
* Margin adjustment may cause screen flickering on some devices and so should be disabled.
|
||||
*/
|
||||
public static final String KEY_TERMINAL_MARGIN_ADJUSTMENT = "terminal_margin_adjustment";
|
||||
public static final boolean DEFAULT_TERMINAL_MARGIN_ADJUSTMENT = true;
|
||||
|
||||
|
||||
/**
|
||||
* 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 the soft keyboard will be enabled only if no hardware keyboard
|
||||
* attached, for cases where users want to use a hardware keyboard instead.
|
||||
*/
|
||||
public static final String KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE = "soft_keyboard_enabled_only_if_no_hardware";
|
||||
public static final boolean DEFAULT_VALUE_KEY_SOFT_KEYBOARD_ENABLED_ONLY_IF_NO_HARDWARE = false;
|
||||
|
||||
|
||||
/**
|
||||
* 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 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:API app constants.
|
||||
*/
|
||||
public static final class TERMUX_API_APP {
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Termux:Boot app constants.
|
||||
*/
|
||||
public static final class TERMUX_BOOT_APP {
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Termux:Float app constants.
|
||||
*/
|
||||
public static final class TERMUX_FLOAT_APP {
|
||||
|
||||
/**
|
||||
* The float window x coordinate.
|
||||
*/
|
||||
public static final String KEY_WINDOW_X = "window_x";
|
||||
|
||||
/**
|
||||
* The float window y coordinate.
|
||||
*/
|
||||
public static final String KEY_WINDOW_Y = "window_y";
|
||||
|
||||
/**
|
||||
* The float window width.
|
||||
*/
|
||||
public static final String KEY_WINDOW_WIDTH = "window_width";
|
||||
|
||||
/**
|
||||
* The float window height.
|
||||
*/
|
||||
public static final String KEY_WINDOW_HEIGHT = "window_height";
|
||||
|
||||
/**
|
||||
* Defines the key for font size of termux terminal view.
|
||||
*/
|
||||
public static final String KEY_FONTSIZE = "fontsize";
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Termux:Styling app constants.
|
||||
*/
|
||||
public static final class TERMUX_STYLING_APP {
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Termux:Tasker app constants.
|
||||
*/
|
||||
public static final class TERMUX_TASKER_APP {
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
|
||||
/**
|
||||
* Defines the key for last used PendingIntent request code.
|
||||
*/
|
||||
public static final String KEY_LAST_PENDING_INTENT_REQUEST_CODE = "last_pending_intent_request_code";
|
||||
public static final int DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE = 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Termux:Widget app constants.
|
||||
*/
|
||||
public static final class TERMUX_WIDGET_APP {
|
||||
|
||||
/**
|
||||
* Defines the key for current log level.
|
||||
*/
|
||||
public static final String KEY_LOG_LEVEL = "log_level";
|
||||
|
||||
/**
|
||||
* Defines the key for current token for shortcuts.
|
||||
*/
|
||||
public static final String KEY_TOKEN = "token";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_STYLING_APP;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
public class TermuxStylingAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TermuxStylingAppSharedPreferences";
|
||||
|
||||
private TermuxStylingAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_STYLING_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxStylingAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxStylingAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_STYLING_PACKAGE_NAME);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxStylingAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_STYLING_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxStylingAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxStylingAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_STYLING_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxStylingAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_STYLING_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_STYLING_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
|
||||
else
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_STYLING_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_STYLING_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_TASKER_APP;
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class TermuxTaskerAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TermuxTaskerAppSharedPreferences";
|
||||
|
||||
private TermuxTaskerAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_TASKER_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxTaskerAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxTaskerAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxTaskerAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_TASKER_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxTaskerAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxTaskerAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_TASKER_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxTaskerAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_TASKER_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLastPendingIntentRequestCode() {
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, TERMUX_TASKER_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE);
|
||||
}
|
||||
|
||||
public void setLastPendingIntentRequestCode(int lastPendingIntentRequestCode) {
|
||||
SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_TASKER_APP.KEY_LAST_PENDING_INTENT_REQUEST_CODE, lastPendingIntentRequestCode, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package com.termux.shared.settings.preferences;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.settings.preferences.TermuxPreferenceConstants.TERMUX_WIDGET_APP;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class TermuxWidgetAppSharedPreferences {
|
||||
|
||||
private final Context mContext;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final SharedPreferences mMultiProcessSharedPreferences;
|
||||
|
||||
|
||||
private static final String LOG_TAG = "TermuxWidgetAppSharedPreferences";
|
||||
|
||||
private TermuxWidgetAppSharedPreferences(@NonNull Context context) {
|
||||
mContext = context;
|
||||
mSharedPreferences = getPrivateSharedPreferences(mContext);
|
||||
mMultiProcessSharedPreferences = getPrivateAndMultiProcessSharedPreferences(mContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Context} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_WIDGET_PACKAGE_NAME}.
|
||||
* @return Returns the {@link TermuxWidgetAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
@Nullable
|
||||
public static TermuxWidgetAppSharedPreferences build(@NonNull final Context context) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackage(context, TermuxConstants.TERMUX_WIDGET_PACKAGE_NAME);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxWidgetAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Context} for a package name.
|
||||
*
|
||||
* @param context The {@link Activity} to use to get the {@link Context} of the
|
||||
* {@link TermuxConstants#TERMUX_WIDGET_PACKAGE_NAME}.
|
||||
* @param exitAppOnError If {@code true} and failed to get package context, then a dialog will
|
||||
* be shown which when dismissed will exit the app.
|
||||
* @return Returns the {@link TermuxWidgetAppSharedPreferences}. This will {@code null} if an exception is raised.
|
||||
*/
|
||||
public static TermuxWidgetAppSharedPreferences build(@NonNull final Context context, final boolean exitAppOnError) {
|
||||
Context termuxTaskerPackageContext = PackageUtils.getContextForPackageOrExitApp(context, TermuxConstants.TERMUX_WIDGET_PACKAGE_NAME, exitAppOnError);
|
||||
if (termuxTaskerPackageContext == null)
|
||||
return null;
|
||||
else
|
||||
return new TermuxWidgetAppSharedPreferences(termuxTaskerPackageContext);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
private static SharedPreferences getPrivateAndMultiProcessSharedPreferences(Context context) {
|
||||
if (context == null) return null;
|
||||
return SharedPreferenceUtils.getPrivateAndMultiProcessSharedPreferences(context, TermuxConstants.TERMUX_WIDGET_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getGeneratedToken(@NonNull Context context) {
|
||||
TermuxWidgetAppSharedPreferences preferences = TermuxWidgetAppSharedPreferences.build(context, true);
|
||||
if (preferences == null) return null;
|
||||
return preferences.getGeneratedToken();
|
||||
}
|
||||
|
||||
public String getGeneratedToken() {
|
||||
String token = SharedPreferenceUtils.getString(mSharedPreferences, TERMUX_WIDGET_APP.KEY_TOKEN, null, true);
|
||||
if (token == null) {
|
||||
token = UUID.randomUUID().toString();
|
||||
SharedPreferenceUtils.setString(mSharedPreferences, TERMUX_WIDGET_APP.KEY_TOKEN, token, true);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public int getLogLevel(boolean readFromFile) {
|
||||
if (readFromFile)
|
||||
return SharedPreferenceUtils.getInt(mMultiProcessSharedPreferences, TERMUX_WIDGET_APP.KEY_LOG_LEVEL, Logger.DEFAULT_LOG_LEVEL);
|
||||
else
|
||||
return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_WIDGET_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_WIDGET_APP.KEY_LOG_LEVEL, logLevel, commitToFile);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,577 @@
|
|||
package com.termux.shared.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.collect.BiMap;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 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.logVerbose(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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
|
||||
* was found in {@link Properties} but was invalid.
|
||||
* @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, boolean logErrorOnInvalidValue) {
|
||||
return (boolean) getBooleanValueForStringValue(key, (String) getProperty(context, propertiesFile, key, null), false, logErrorOnInvalidValue, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
|
||||
* was found in {@link Properties} but was invalid.
|
||||
* @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, boolean logErrorOnInvalidValue) {
|
||||
return (boolean) getInvertedBooleanValueForStringValue(key, (String) getProperty(context, propertiesFile, key, null), true, logErrorOnInvalidValue, LOG_TAG);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
|
||||
* was not {@code null} and was invalid.
|
||||
* @param logTag If log tag to use for logging errors.
|
||||
* @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 key, String value, boolean def, boolean logErrorOnInvalidValue, String logTag) {
|
||||
return (boolean) getDefaultIfNotInMap(key, MAP_GENERIC_BOOLEAN, toLowerCase(value), def, logErrorOnInvalidValue, logTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
|
||||
* was not {@code null} and was invalid.
|
||||
* @param logTag If log tag to use for logging errors.
|
||||
* @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 key, String value, boolean def, boolean logErrorOnInvalidValue, String logTag) {
|
||||
return (boolean) getDefaultIfNotInMap(key, MAP_GENERIC_INVERTED_BOOLEAN, toLowerCase(value), def, logErrorOnInvalidValue, logTag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value for the {@code inputValue} {@link Object} key from a {@link BiMap<>}, otherwise
|
||||
* default value if key not found in {@code map}.
|
||||
*
|
||||
* @param key The shared properties {@link String} key value for which the value is being returned.
|
||||
* @param map The {@link BiMap<>} value to get the value from.
|
||||
* @param inputValue The {@link Object} key value of the map.
|
||||
* @param defaultOutputValue The default {@link boolean} value to return if {@code inputValue} not found in map.
|
||||
* The default value must exist as a value in the {@link BiMap<>} passed.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code inputValue}
|
||||
* was not {@code null} and was not found in the map.
|
||||
* @param logTag If log tag to use for logging errors.
|
||||
* @return Returns the value for the {@code inputValue} key from the map if it exists. Otherwise
|
||||
* returns default value.
|
||||
*/
|
||||
public static Object getDefaultIfNotInMap(String key, @NonNull BiMap<?, ?> map, Object inputValue, Object defaultOutputValue, boolean logErrorOnInvalidValue, String logTag) {
|
||||
Object outputValue = map.get(inputValue);
|
||||
if (outputValue == null) {
|
||||
Object defaultInputValue = map.inverse().get(defaultOutputValue);
|
||||
if (defaultInputValue == null)
|
||||
Logger.logError(LOG_TAG, "The default output value \"" + defaultOutputValue + "\" for the key \"" + key + "\" does not exist as a value in the BiMap passed to getDefaultIfNotInMap(): " + map.values());
|
||||
|
||||
if (logErrorOnInvalidValue && inputValue != null) {
|
||||
if (key != null)
|
||||
Logger.logError(logTag, "The value \"" + inputValue + "\" for the key \"" + key + "\" is invalid. Using default value \"" + defaultInputValue + "\" instead.");
|
||||
else
|
||||
Logger.logError(logTag, "The value \"" + inputValue + "\" is invalid. Using default value \"" + defaultInputValue + "\" instead.");
|
||||
}
|
||||
|
||||
return defaultOutputValue;
|
||||
} else {
|
||||
return outputValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code int} {@code value} as is if between {@code min} and {@code max} (inclusive), otherwise
|
||||
* return default value.
|
||||
*
|
||||
* @param key The shared properties {@link String} key value for which the value is being returned.
|
||||
* @param value The {@code int} value to check.
|
||||
* @param def The default {@code int} value if {@code value} not in range.
|
||||
* @param min The min allowed {@code int} value.
|
||||
* @param max The max allowed {@code int} value.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
|
||||
* not in range.
|
||||
* @param ignoreErrorIfValueZero If logging error should be ignored if value equals 0.
|
||||
* @param logTag If log tag to use for logging errors.
|
||||
* @return Returns the {@code value} as is if within range. Otherwise returns default value.
|
||||
*/
|
||||
public static int getDefaultIfNotInRange(String key, int value, int def, int min, int max, boolean logErrorOnInvalidValue, boolean ignoreErrorIfValueZero, String logTag) {
|
||||
if (value < min || value > max) {
|
||||
if (logErrorOnInvalidValue && (!ignoreErrorIfValueZero || value != 0)) {
|
||||
if (key != null)
|
||||
Logger.logError(logTag, "The value \"" + value + "\" for the key \"" + key + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
|
||||
else
|
||||
Logger.logError(logTag, "The value \"" + value + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
|
||||
}
|
||||
return def;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code float} {@code value} as is if between {@code min} and {@code max} (inclusive), otherwise
|
||||
* return default value.
|
||||
*
|
||||
* @param key The shared properties {@link String} key value for which the value is being returned.
|
||||
* @param value The {@code float} value to check.
|
||||
* @param def The default {@code float} value if {@code value} not in range.
|
||||
* @param min The min allowed {@code float} value.
|
||||
* @param max The max allowed {@code float} value.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if {@code value}
|
||||
* not in range.
|
||||
* @param ignoreErrorIfValueZero If logging error should be ignored if value equals 0.
|
||||
* @param logTag If log tag to use for logging errors.
|
||||
* @return Returns the {@code value} as is if within range. Otherwise returns default value.
|
||||
*/
|
||||
public static float getDefaultIfNotInRange(String key, float value, float def, float min, float max, boolean logErrorOnInvalidValue, boolean ignoreErrorIfValueZero, String logTag) {
|
||||
if (value < min || value > max) {
|
||||
if (logErrorOnInvalidValue && (!ignoreErrorIfValueZero || value != 0)) {
|
||||
if (key != null)
|
||||
Logger.logError(logTag, "The value \"" + value + "\" for the key \"" + key + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
|
||||
else
|
||||
Logger.logError(logTag, "The value \"" + value + "\" is not within the range " + min + "-" + max + " (inclusive). Using default value \"" + def + "\" instead.");
|
||||
}
|
||||
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(@Nullable T object, @Nullable T def) {
|
||||
return (object == null) ? def : object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link String} object itself if it is not {@code null} or empty, otherwise default.
|
||||
*
|
||||
* @param object The {@link String} to check.
|
||||
* @param def The default {@link String}.
|
||||
* @return Returns {@code object} if it is not {@code null}, otherwise returns {@code def}.
|
||||
*/
|
||||
public static String getDefaultIfNullOrEmpty(@Nullable String object, @Nullable String def) {
|
||||
return (object == null || object.isEmpty()) ? 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,462 @@
|
|||
package com.termux.shared.settings.properties;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.collect.ImmutableBiMap;
|
||||
import com.termux.shared.termux.TermuxConstants;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/*
|
||||
* Version: v0.15.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.
|
||||
*
|
||||
* - 0.7.0 (2021-05-09)
|
||||
* - Add `*SOFT_KEYBOARD_TOGGLE_BEHAVIOUR*`.
|
||||
*
|
||||
* - 0.8.0 (2021-05-10)
|
||||
* - Change the `KEY_USE_BACK_KEY_AS_ESCAPE_KEY` and `KEY_VIRTUAL_VOLUME_KEYS_DISABLED` booleans
|
||||
* to `KEY_BACK_KEY_BEHAVIOUR` and `KEY_VOLUME_KEYS_BEHAVIOUR` String internal values.
|
||||
* - Renamed `SOFT_KEYBOARD_TOGGLE_BEHAVIOUR` to `KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`.
|
||||
*
|
||||
* - 0.9.0 (2021-05-14)
|
||||
* - Add `*KEY_TERMINAL_CURSOR_BLINK_RATE*`.
|
||||
*
|
||||
* - 0.10.0 (2021-05-15)
|
||||
* - Add `MAP_BACK_KEY_BEHAVIOUR`, `MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR`, `MAP_VOLUME_KEYS_BEHAVIOUR`.
|
||||
*
|
||||
* - 0.11.0 (2021-06-10)
|
||||
* - Add `*KEY_TERMINAL_TRANSCRIPT_ROWS*`.
|
||||
*
|
||||
* - 0.12.0 (2021-06-10)
|
||||
* - Add `*KEY_TERMINAL_CURSOR_STYLE*`.
|
||||
*
|
||||
* - 0.13.0 (2021-08-25)
|
||||
* - Add `*KEY_TERMINAL_MARGIN_HORIZONTAL*` and `*KEY_TERMINAL_MARGIN_VERTICAL*`.
|
||||
*
|
||||
* - 0.14.0 (2021-09-02)
|
||||
* - Add `getTermuxFloatPropertiesFile()`.
|
||||
*
|
||||
* - 0.15.0 (2021-09-05)
|
||||
* - Add `KEY_EXTRA_KEYS_TEXT_ALL_CAPS`.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
/* boolean */
|
||||
|
||||
/** Defines the key for whether hardware keyboard shortcuts are enabled. */
|
||||
public static final String KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS = "disable-hardware-keyboard-shortcuts"; // Default: "disable-hardware-keyboard-shortcuts"
|
||||
|
||||
|
||||
/** Defines the key for whether a toast will be shown when user changes the terminal session */
|
||||
public static final String KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST = "disable-terminal-session-change-toast"; // Default: "disable-terminal-session-change-toast"
|
||||
|
||||
|
||||
|
||||
/** 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 text for the extra keys buttons should be all capitalized automatically */
|
||||
public static final String KEY_EXTRA_KEYS_TEXT_ALL_CAPS = "extra-keys-text-all-caps"; // Default: "extra-keys-text-all-caps"
|
||||
|
||||
|
||||
|
||||
/** 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 url links in terminal transcript will automatically open on click or on tap */
|
||||
public static final String KEY_TERMINAL_ONCLICK_URL_OPEN = "terminal-onclick-url-open"; // Default: "terminal-onclick-url-open"
|
||||
|
||||
|
||||
|
||||
/** 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"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* int */
|
||||
|
||||
/** 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 terminal cursor blink rate */
|
||||
public static final String KEY_TERMINAL_CURSOR_BLINK_RATE = "terminal-cursor-blink-rate"; // Default: "terminal-cursor-blink-rate"
|
||||
public static final int IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN = TerminalView.TERMINAL_CURSOR_BLINK_RATE_MIN;
|
||||
public static final int IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX = TerminalView.TERMINAL_CURSOR_BLINK_RATE_MAX;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE = 0;
|
||||
|
||||
|
||||
|
||||
/** Defines the key for the terminal cursor style */
|
||||
public static final String KEY_TERMINAL_CURSOR_STYLE = "terminal-cursor-style"; // Default: "terminal-cursor-style"
|
||||
|
||||
public static final String VALUE_TERMINAL_CURSOR_STYLE_BLOCK = "block";
|
||||
public static final String VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = "underline";
|
||||
public static final String VALUE_TERMINAL_CURSOR_STYLE_BAR = "bar";
|
||||
|
||||
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BLOCK = TerminalEmulator.TERMINAL_CURSOR_STYLE_BLOCK;
|
||||
public static final int IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE = TerminalEmulator.TERMINAL_CURSOR_STYLE_UNDERLINE;
|
||||
public static final int IVALUE_TERMINAL_CURSOR_STYLE_BAR = TerminalEmulator.TERMINAL_CURSOR_STYLE_BAR;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE = TerminalEmulator.DEFAULT_TERMINAL_CURSOR_STYLE;
|
||||
|
||||
/** Defines the bidirectional map for terminal cursor styles and their internal values */
|
||||
public static final ImmutableBiMap<String, Integer> MAP_TERMINAL_CURSOR_STYLE =
|
||||
new ImmutableBiMap.Builder<String, Integer>()
|
||||
.put(VALUE_TERMINAL_CURSOR_STYLE_BLOCK, IVALUE_TERMINAL_CURSOR_STYLE_BLOCK)
|
||||
.put(VALUE_TERMINAL_CURSOR_STYLE_UNDERLINE, IVALUE_TERMINAL_CURSOR_STYLE_UNDERLINE)
|
||||
.put(VALUE_TERMINAL_CURSOR_STYLE_BAR, IVALUE_TERMINAL_CURSOR_STYLE_BAR)
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
/** Defines the key for the terminal margin on left and right in dp units */
|
||||
public static final String KEY_TERMINAL_MARGIN_HORIZONTAL = "terminal-margin-horizontal"; // Default: "terminal-margin-horizontal"
|
||||
public static final int IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN = 0;
|
||||
public static final int IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX = 100;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN = 3;
|
||||
|
||||
/** Defines the key for the terminal margin on top and bottom in dp units */
|
||||
public static final String KEY_TERMINAL_MARGIN_VERTICAL = "terminal-margin-vertical"; // Default: "terminal-margin-vertical"
|
||||
public static final int IVALUE_TERMINAL_MARGIN_VERTICAL_MIN = 0;
|
||||
public static final int IVALUE_TERMINAL_MARGIN_VERTICAL_MAX = 100;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN = 0;
|
||||
|
||||
|
||||
|
||||
/** Defines the key for the terminal transcript rows */
|
||||
public static final String KEY_TERMINAL_TRANSCRIPT_ROWS = "terminal-transcript-rows"; // Default: "terminal-transcript-rows"
|
||||
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MIN;
|
||||
public static final int IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX = TerminalEmulator.TERMINAL_TRANSCRIPT_ROWS_MAX;
|
||||
public static final int DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS = TerminalEmulator.DEFAULT_TERMINAL_TRANSCRIPT_ROWS;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* float */
|
||||
|
||||
/** Defines the key for the terminal toolbar height */
|
||||
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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* Integer */
|
||||
|
||||
/** 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();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* String */
|
||||
|
||||
/** Defines the key for whether back key will behave as escape key or literal back key */
|
||||
public static final String KEY_BACK_KEY_BEHAVIOUR = "back-key"; // Default: "back-key"
|
||||
|
||||
public static final String IVALUE_BACK_KEY_BEHAVIOUR_BACK = "back";
|
||||
public static final String IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE = "escape";
|
||||
public static final String DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR = IVALUE_BACK_KEY_BEHAVIOUR_BACK;
|
||||
|
||||
/** Defines the bidirectional map for back key behaviour values and their internal values */
|
||||
public static final ImmutableBiMap<String, String> MAP_BACK_KEY_BEHAVIOUR =
|
||||
new ImmutableBiMap.Builder<String, String>()
|
||||
.put(IVALUE_BACK_KEY_BEHAVIOUR_BACK, IVALUE_BACK_KEY_BEHAVIOUR_BACK)
|
||||
.put(IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE, IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE)
|
||||
.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"
|
||||
//public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]"; // Single row
|
||||
public static final String DEFAULT_IVALUE_EXTRA_KEYS = "[['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'], ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]"; // Double row
|
||||
|
||||
/** 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_STYLE = "default";
|
||||
|
||||
|
||||
|
||||
/** Defines the key for whether toggle soft keyboard request will show/hide or enable/disable keyboard */
|
||||
public static final String KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR = "soft-keyboard-toggle-behaviour"; // Default: "soft-keyboard-toggle-behaviour"
|
||||
|
||||
public static final String IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE = "show/hide";
|
||||
public static final String IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE = "enable/disable";
|
||||
public static final String DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR = IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE;
|
||||
|
||||
/** Defines the bidirectional map for toggle soft keyboard behaviour values and their internal values */
|
||||
public static final ImmutableBiMap<String, String> MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR =
|
||||
new ImmutableBiMap.Builder<String, String>()
|
||||
.put(IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE, IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_SHOW_HIDE)
|
||||
.put(IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE, IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE)
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
/** Defines the key for whether volume keys will behave as virtual or literal volume keys */
|
||||
public static final String KEY_VOLUME_KEYS_BEHAVIOUR = "volume-keys"; // Default: "volume-keys"
|
||||
|
||||
public static final String IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL = "virtual";
|
||||
public static final String IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME = "volume";
|
||||
public static final String DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR = IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL;
|
||||
|
||||
/** Defines the bidirectional map for volume keys behaviour values and their internal values */
|
||||
public static final ImmutableBiMap<String, String> MAP_VOLUME_KEYS_BEHAVIOUR =
|
||||
new ImmutableBiMap.Builder<String, String>()
|
||||
.put(IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL, IVALUE_VOLUME_KEY_BEHAVIOUR_VIRTUAL)
|
||||
.put(IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME, IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME)
|
||||
.build();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/** 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_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS,
|
||||
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||
KEY_EXTRA_KEYS_TEXT_ALL_CAPS,
|
||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||
KEY_TERMINAL_ONCLICK_URL_OPEN,
|
||||
KEY_USE_BLACK_UI,
|
||||
KEY_USE_CTRL_SPACE_WORKAROUND,
|
||||
KEY_USE_FULLSCREEN,
|
||||
KEY_USE_FULLSCREEN_WORKAROUND,
|
||||
TermuxConstants.PROP_ALLOW_EXTERNAL_APPS,
|
||||
|
||||
/* int */
|
||||
KEY_BELL_BEHAVIOUR,
|
||||
KEY_TERMINAL_CURSOR_BLINK_RATE,
|
||||
KEY_TERMINAL_CURSOR_STYLE,
|
||||
KEY_TERMINAL_MARGIN_HORIZONTAL,
|
||||
KEY_TERMINAL_MARGIN_VERTICAL,
|
||||
KEY_TERMINAL_TRANSCRIPT_ROWS,
|
||||
|
||||
/* 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_BACK_KEY_BEHAVIOUR,
|
||||
KEY_DEFAULT_WORKING_DIRECTORY,
|
||||
KEY_EXTRA_KEYS,
|
||||
KEY_EXTRA_KEYS_STYLE,
|
||||
KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR,
|
||||
KEY_VOLUME_KEYS_BEHAVIOUR
|
||||
));
|
||||
|
||||
/** Defines the set for keys loaded by termux that have default boolean behaviour with false as default.
|
||||
* "true" -> true
|
||||
* "false" -> false
|
||||
* default: false
|
||||
*/
|
||||
public static final Set<String> TERMUX_DEFAULT_FALSE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS,
|
||||
KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST,
|
||||
KEY_ENFORCE_CHAR_BASED_INPUT,
|
||||
KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP,
|
||||
KEY_TERMINAL_ONCLICK_URL_OPEN,
|
||||
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 boolean behaviour with true as default.
|
||||
* "true" -> true
|
||||
* "false" -> false
|
||||
* default: true
|
||||
*/
|
||||
public static final Set<String> TERMUX_DEFAULT_TRUE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
KEY_EXTRA_KEYS_TEXT_ALL_CAPS
|
||||
));
|
||||
|
||||
/** Defines the set for keys loaded by termux that have default inverted boolean behaviour with false as default.
|
||||
* "false" -> true
|
||||
* "true" -> false
|
||||
* default: false
|
||||
*/
|
||||
public static final Set<String> TERMUX_DEFAULT_INVERETED_FALSE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST = new HashSet<>(Arrays.asList(
|
||||
));
|
||||
|
||||
/** Defines the set for keys loaded by termux that have default inverted boolean behaviour with true as default.
|
||||
* "false" -> true
|
||||
* "true" -> false
|
||||
* default: true
|
||||
*/
|
||||
public static final Set<String> TERMUX_DEFAULT_INVERETED_TRUE_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() {
|
||||
return getPropertiesFile(new String[]{
|
||||
TermuxConstants.TERMUX_PROPERTIES_PRIMARY_FILE_PATH,
|
||||
TermuxConstants.TERMUX_PROPERTIES_SECONDARY_FILE_PATH
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the first {@link File} found at
|
||||
* {@link TermuxConstants#TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH} or
|
||||
* {@link TermuxConstants#TERMUX_FLOAT_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 getTermuxFloatPropertiesFile() {
|
||||
return getPropertiesFile(new String[]{
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_PRIMARY_FILE_PATH,
|
||||
TermuxConstants.TERMUX_FLOAT_PROPERTIES_SECONDARY_FILE_PATH
|
||||
});
|
||||
}
|
||||
|
||||
public static File getPropertiesFile(@NonNull String[] possiblePropertiesFileLocations) {
|
||||
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 properties file found at: " + Arrays.toString(possiblePropertiesFileLocations));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,632 @@
|
|||
package com.termux.shared.settings.properties;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
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 java.util.Set;
|
||||
|
||||
public abstract class TermuxSharedProperties {
|
||||
|
||||
protected final Context mContext;
|
||||
protected final String mLabel;
|
||||
protected final File mPropertiesFile;
|
||||
protected final SharedProperties mSharedProperties;
|
||||
|
||||
public static final String LOG_TAG = "TermuxSharedProperties";
|
||||
|
||||
public TermuxSharedProperties(@NonNull Context context, @NonNull String label, File propertiesFile,
|
||||
@NonNull Set<String> propertiesList, @NonNull SharedPropertiesParser sharedPropertiesParser) {
|
||||
mContext = context;
|
||||
mLabel = label;
|
||||
mPropertiesFile = propertiesFile;
|
||||
mSharedProperties = new SharedProperties(context, mPropertiesFile, propertiesList, sharedPropertiesParser);
|
||||
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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
|
||||
* was found in {@link Properties} but was invalid.
|
||||
* @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, boolean logErrorOnInvalidValue) {
|
||||
return (boolean) SharedProperties.getBooleanValueForStringValue(key, (String) getPropertyValue(key, null, cached), false, logErrorOnInvalidValue, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param logErrorOnInvalidValue If {@code true}, then an error will be logged if key value
|
||||
* was found in {@link Properties} but was invalid.
|
||||
* @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, boolean logErrorOnInvalidValue) {
|
||||
return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(key, (String) getPropertyValue(key, null, cached), true, logErrorOnInvalidValue, LOG_TAG);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 = getInternalTermuxPropertyValueFromValue(mContext, key, null);
|
||||
Logger.logWarn(LOG_TAG, "The value for \"" + key + "\" not found in SharedProperties cache, force returning default value: `" + value + "`");
|
||||
return value;
|
||||
}
|
||||
} else {
|
||||
// We get the property value directly from file and return its internal value
|
||||
return getInternalTermuxPropertyValueFromValue(mContext, key, mSharedProperties.getProperty(key, false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get the internal {@link Object} value for the key passed from the file returned by
|
||||
* {@code propertiesFile}. The {@link Properties} object is
|
||||
* read directly from the file and internal value is returned for the property value against the key.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @param key The key for which the internal object is required.
|
||||
* @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 static Object getInternalPropertyValue(Context context, File propertiesFile, String key,
|
||||
@NonNull SharedPropertiesParser sharedPropertiesParser) {
|
||||
return SharedProperties.getInternalProperty(context, propertiesFile, key, sharedPropertiesParser);
|
||||
}
|
||||
|
||||
/**
|
||||
* The class that implements the {@link SharedPropertiesParser} interface.
|
||||
*/
|
||||
public static class SharedPropertiesParserClient implements SharedPropertiesParser {
|
||||
/**
|
||||
* 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_BLACK_UI:
|
||||
return (boolean) getUseBlackUIInternalPropertyValueFromValue(context, value);
|
||||
|
||||
/* int */
|
||||
case TermuxPropertyConstants.KEY_BELL_BEHAVIOUR:
|
||||
return (int) getBellBehaviourInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE:
|
||||
return (int) getTerminalCursorBlinkRateInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE:
|
||||
return (int) getTerminalCursorStyleInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL:
|
||||
return (int) getTerminalMarginHorizontalInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL:
|
||||
return (int) getTerminalMarginVerticalInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS:
|
||||
return (int) getTerminalTranscriptRowsInternalPropertyValueFromValue(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_BACK_KEY_BEHAVIOUR:
|
||||
return (String) getBackKeyBehaviourInternalPropertyValueFromValue(value);
|
||||
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);
|
||||
case TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR:
|
||||
return (String) getSoftKeyboardToggleBehaviourInternalPropertyValueFromValue(value);
|
||||
case TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR:
|
||||
return (String) getVolumeKeysBehaviourInternalPropertyValueFromValue(value);
|
||||
|
||||
default:
|
||||
// default false boolean behaviour
|
||||
if (TermuxPropertyConstants.TERMUX_DEFAULT_FALSE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
|
||||
return (boolean) SharedProperties.getBooleanValueForStringValue(key, value, false, true, LOG_TAG);
|
||||
// default true boolean behaviour
|
||||
if (TermuxPropertyConstants.TERMUX_DEFAULT_TRUE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
|
||||
return (boolean) SharedProperties.getBooleanValueForStringValue(key, value, true, true, LOG_TAG);
|
||||
// default inverted false boolean behaviour
|
||||
//else if (TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_FALSE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
|
||||
// return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(key, value, false, true, LOG_TAG);
|
||||
// default inverted true boolean behaviour
|
||||
// else if (TermuxPropertyConstants.TERMUX_DEFAULT_INVERETED_TRUE_BOOLEAN_BEHAVIOUR_PROPERTIES_LIST.contains(key))
|
||||
// return (boolean) SharedProperties.getInvertedBooleanValueForStringValue(key, value, true, true, LOG_TAG);
|
||||
// just use String object as is (may be null)
|
||||
else
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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(TermuxPropertyConstants.KEY_USE_BLACK_UI, value, nightMode == Configuration.UI_MODE_NIGHT_YES, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {@link 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 (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, TermuxPropertyConstants.MAP_BELL_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_BELL_BEHAVIOUR, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalCursorBlinkRateInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_BLINK_RATE,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_CURSOR_BLINK_RATE_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_CURSOR_BLINK_RATE_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal value after mapping it based on
|
||||
* {@link TermuxPropertyConstants#MAP_TERMINAL_CURSOR_STYLE} if the value is not {@code null}
|
||||
* and is valid, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalCursorStyleInternalPropertyValueFromValue(String value) {
|
||||
return (int) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, TermuxPropertyConstants.MAP_TERMINAL_CURSOR_STYLE, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_CURSOR_STYLE, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalMarginHorizontalInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_HORIZONTAL_MARGIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_HORIZONTAL_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_HORIZONTAL_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_VERTICAL_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_MARGIN_VERTICAL_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalMarginVerticalInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_VERTICAL_MARGIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_VERTICAL_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_MARGIN_VERTICAL_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX},
|
||||
* otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS}.
|
||||
*
|
||||
* @param value The {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static int getTerminalTranscriptRowsInternalPropertyValueFromValue(String value) {
|
||||
return SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS,
|
||||
DataUtils.getIntFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TRANSCRIPT_ROWS,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_TRANSCRIPT_ROWS_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the int for the value if its not null and is between
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN} and
|
||||
* {@link TermuxPropertyConstants#IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX},
|
||||
* otherwise returns {@link 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 SharedProperties.getDefaultIfNotInRange(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
|
||||
DataUtils.getFloatFromString(value, TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR),
|
||||
TermuxPropertyConstants.DEFAULT_IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MIN,
|
||||
TermuxPropertyConstants.IVALUE_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR_MAX,
|
||||
true, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR}.
|
||||
*
|
||||
* @param value {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static String getBackKeyBehaviourInternalPropertyValueFromValue(String value) {
|
||||
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_BACK_KEY_BEHAVIOUR, TermuxPropertyConstants.MAP_BACK_KEY_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_BACK_KEY_BEHAVIOUR, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
// is not a directory or is not readable.
|
||||
Logger.logError(LOG_TAG, "The path \"" + path + "\" for the key \"" + TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY + "\" does not exist, is not a directory or is not readable. Using default value \"" + TermuxPropertyConstants.DEFAULT_IVALUE_DEFAULT_WORKING_DIRECTORY + "\" instead.");
|
||||
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.getDefaultIfNullOrEmpty(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.getDefaultIfNullOrEmpty(value, TermuxPropertyConstants.DEFAULT_IVALUE_EXTRA_KEYS_STYLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR}.
|
||||
*
|
||||
* @param value {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static String getSoftKeyboardToggleBehaviourInternalPropertyValueFromValue(String value) {
|
||||
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, TermuxPropertyConstants.MAP_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, true, LOG_TAG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value itself if it is not {@code null}, otherwise returns {@link TermuxPropertyConstants#DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR}.
|
||||
*
|
||||
* @param value {@link String} value to convert.
|
||||
* @return Returns the internal value for value.
|
||||
*/
|
||||
public static String getVolumeKeysBehaviourInternalPropertyValueFromValue(String value) {
|
||||
return (String) SharedProperties.getDefaultIfNotInMap(TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR, TermuxPropertyConstants.MAP_VOLUME_KEYS_BEHAVIOUR, SharedProperties.toLowerCase(value), TermuxPropertyConstants.DEFAULT_IVALUE_VOLUME_KEYS_BEHAVIOUR, true, LOG_TAG);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public boolean areHardwareKeyboardShortcutsDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_HARDWARE_KEYBOARD_SHORTCUTS, true);
|
||||
}
|
||||
|
||||
public boolean areTerminalSessionChangeToastsDisabled() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_DISABLE_TERMINAL_SESSION_CHANGE_TOAST, true);
|
||||
}
|
||||
|
||||
public boolean isEnforcingCharBasedInput() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_ENFORCE_CHAR_BASED_INPUT, true);
|
||||
}
|
||||
|
||||
public boolean shouldExtraKeysTextBeAllCaps() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_EXTRA_KEYS_TEXT_ALL_CAPS, true);
|
||||
}
|
||||
|
||||
public boolean shouldSoftKeyboardBeHiddenOnStartup() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_HIDE_SOFT_KEYBOARD_ON_STARTUP, true);
|
||||
}
|
||||
|
||||
public boolean shouldOpenTerminalTranscriptURLOnClick() {
|
||||
return (boolean) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_ONCLICK_URL_OPEN, 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 int getBellBehaviour() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_BELL_BEHAVIOUR, true);
|
||||
}
|
||||
|
||||
public int getTerminalCursorBlinkRate() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_BLINK_RATE, true);
|
||||
}
|
||||
|
||||
public int getTerminalCursorStyle() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_CURSOR_STYLE, true);
|
||||
}
|
||||
|
||||
public int getTerminalMarginHorizontal() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_HORIZONTAL, true);
|
||||
}
|
||||
|
||||
public int getTerminalMarginVertical() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_MARGIN_VERTICAL, true);
|
||||
}
|
||||
|
||||
public int getTerminalTranscriptRows() {
|
||||
return (int) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TRANSCRIPT_ROWS, true);
|
||||
}
|
||||
|
||||
public float getTerminalToolbarHeightScaleFactor() {
|
||||
return (float) getInternalPropertyValue(TermuxPropertyConstants.KEY_TERMINAL_TOOLBAR_HEIGHT_SCALE_FACTOR, true);
|
||||
}
|
||||
|
||||
public boolean isBackKeyTheEscapeKey() {
|
||||
return (boolean) TermuxPropertyConstants.IVALUE_BACK_KEY_BEHAVIOUR_ESCAPE.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_BACK_KEY_BEHAVIOUR, true));
|
||||
}
|
||||
|
||||
public String getDefaultWorkingDirectory() {
|
||||
return (String) getInternalPropertyValue(TermuxPropertyConstants.KEY_DEFAULT_WORKING_DIRECTORY, true);
|
||||
}
|
||||
|
||||
public boolean shouldEnableDisableSoftKeyboardOnToggle() {
|
||||
return (boolean) TermuxPropertyConstants.IVALUE_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR_ENABLE_DISABLE.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_SOFT_KEYBOARD_TOGGLE_BEHAVIOUR, true));
|
||||
}
|
||||
|
||||
public boolean areVirtualVolumeKeysDisabled() {
|
||||
return (boolean) TermuxPropertyConstants.IVALUE_VOLUME_KEY_BEHAVIOUR_VOLUME.equals(getInternalPropertyValue(TermuxPropertyConstants.KEY_VOLUME_KEYS_BEHAVIOUR, true));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public void dumpPropertiesToLog() {
|
||||
Properties properties = getProperties(true);
|
||||
StringBuilder propertiesDump = new StringBuilder();
|
||||
|
||||
propertiesDump.append(mLabel).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(mLabel).append(" 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,352 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.ResultConfig;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.FunctionErrno;
|
||||
import com.termux.shared.models.errors.ResultSenderErrno;
|
||||
import com.termux.shared.termux.AndroidUtils;
|
||||
import com.termux.shared.termux.TermuxConstants.RESULT_SENDER;
|
||||
|
||||
public class ResultSender {
|
||||
|
||||
private static final String LOG_TAG = "ResultSender";
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller via
|
||||
* {@link ResultConfig#resultPendingIntent} and/or by writing it to files in
|
||||
* {@link ResultConfig#resultDirectoryPath}. If both are not {@code null}, then result will be
|
||||
* sent via both.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
|
||||
* should be logged.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultData(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
|
||||
if (context == null || resultConfig == null || resultData == null)
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETERS.getError("context, resultConfig or resultData", "sendCommandResultData");
|
||||
|
||||
Error error;
|
||||
|
||||
if (resultConfig.resultPendingIntent != null) {
|
||||
error = sendCommandResultDataWithPendingIntent(context, logTag, label, resultConfig, resultData, logStdoutAndStderr);
|
||||
if (error != null || resultConfig.resultDirectoryPath == null)
|
||||
return error;
|
||||
}
|
||||
|
||||
if (resultConfig.resultDirectoryPath != null) {
|
||||
return sendCommandResultDataToDirectory(context, logTag, label, resultConfig, resultData, logStdoutAndStderr);
|
||||
} else {
|
||||
return FunctionErrno.ERRNO_UNSET_PARAMETERS.getError("resultConfig.resultPendingIntent or resultConfig.resultDirectoryPath", "sendCommandResultData");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller via {@link ResultConfig#resultPendingIntent}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
|
||||
* should be logged.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultDataWithPendingIntent(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
|
||||
if (context == null || resultConfig == null || resultData == null || resultConfig.resultPendingIntent == null || resultConfig.resultBundleKey == null)
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData, resultConfig.resultPendingIntent or resultConfig.resultBundleKey", "sendCommandResultDataWithPendingIntent");
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Logger.logDebugExtended(logTag, "Sending result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + ResultData.getResultDataLogString(resultData, logStdoutAndStderr));
|
||||
|
||||
String resultDataStdout = resultData.stdout.toString();
|
||||
String resultDataStderr = resultData.stderr.toString();
|
||||
|
||||
String truncatedStdout = null;
|
||||
String truncatedStderr = null;
|
||||
|
||||
String stdoutOriginalLength = String.valueOf(resultDataStdout.length());
|
||||
String stderrOriginalLength = String.valueOf(resultDataStderr.length());
|
||||
|
||||
// Truncate stdout and stdout to max TRANSACTION_SIZE_LIMIT_IN_BYTES
|
||||
if (resultDataStderr.isEmpty()) {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else if (resultDataStdout.isEmpty()) {
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES, false, false, false);
|
||||
} else {
|
||||
truncatedStdout = DataUtils.getTruncatedCommandOutput(resultDataStdout, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
truncatedStderr = DataUtils.getTruncatedCommandOutput(resultDataStderr, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 2, false, false, false);
|
||||
}
|
||||
|
||||
if (truncatedStdout != null && truncatedStdout.length() < resultDataStdout.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" stdout length truncated from " + stdoutOriginalLength + " to " + truncatedStdout.length());
|
||||
resultDataStdout = truncatedStdout;
|
||||
}
|
||||
|
||||
if (truncatedStderr != null && truncatedStderr.length() < resultDataStderr.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" stderr length truncated from " + stderrOriginalLength + " to " + truncatedStderr.length());
|
||||
resultDataStderr = truncatedStderr;
|
||||
}
|
||||
|
||||
String resultDataErrmsg = null;
|
||||
if (resultData.isStateFailed()) {
|
||||
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||
if (resultDataErrmsg.isEmpty()) resultDataErrmsg = null;
|
||||
}
|
||||
|
||||
String errmsgOriginalLength = (resultDataErrmsg == null) ? null : String.valueOf(resultDataErrmsg.length());
|
||||
|
||||
// Truncate error to max TRANSACTION_SIZE_LIMIT_IN_BYTES / 4
|
||||
// trim from end to preserve start of stacktraces
|
||||
String truncatedErrmsg = DataUtils.getTruncatedCommandOutput(resultDataErrmsg, DataUtils.TRANSACTION_SIZE_LIMIT_IN_BYTES / 4, true, false, false);
|
||||
if (truncatedErrmsg != null && truncatedErrmsg.length() < resultDataErrmsg.length()) {
|
||||
Logger.logWarn(logTag, "The result for command \"" + label + "\" error length truncated from " + errmsgOriginalLength + " to " + truncatedErrmsg.length());
|
||||
resultDataErrmsg = truncatedErrmsg;
|
||||
}
|
||||
|
||||
|
||||
final Bundle resultBundle = new Bundle();
|
||||
resultBundle.putString(resultConfig.resultStdoutKey, resultDataStdout);
|
||||
resultBundle.putString(resultConfig.resultStdoutOriginalLengthKey, stdoutOriginalLength);
|
||||
resultBundle.putString(resultConfig.resultStderrKey, resultDataStderr);
|
||||
resultBundle.putString(resultConfig.resultStderrOriginalLengthKey, stderrOriginalLength);
|
||||
if (resultData.exitCode != null)
|
||||
resultBundle.putInt(resultConfig.resultExitCodeKey, resultData.exitCode);
|
||||
resultBundle.putInt(resultConfig.resultErrCodeKey, resultData.getErrCode());
|
||||
resultBundle.putString(resultConfig.resultErrmsgKey, resultDataErrmsg);
|
||||
|
||||
Intent resultIntent = new Intent();
|
||||
resultIntent.putExtra(resultConfig.resultBundleKey, resultBundle);
|
||||
|
||||
try {
|
||||
resultConfig.resultPendingIntent.send(context, Activity.RESULT_OK, resultIntent);
|
||||
} catch (PendingIntent.CanceledException e) {
|
||||
// The caller doesn't want the result? That's fine, just ignore
|
||||
Logger.logDebug(logTag, "The command \"" + label + "\" creator " + resultConfig.resultPendingIntent.getCreatorPackage() + " does not want the results anymore");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Send result stored in {@link ResultConfig} to command caller by writing it to files in
|
||||
* {@link ResultConfig#resultDirectoryPath}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param logTag The log tag to use for logging.
|
||||
* @param label The label for the command.
|
||||
* @param resultConfig The {@link ResultConfig} object containing information on how to send the result.
|
||||
* @param resultData The {@link ResultData} object containing result data.
|
||||
* @param logStdoutAndStderr Set to {@code true} if {@link ResultData#stdout} and {@link ResultData#stderr}
|
||||
* should be logged.
|
||||
* @return Returns the {@link Error} if failed to send the result, otherwise {@code null}.
|
||||
*/
|
||||
public static Error sendCommandResultDataToDirectory(Context context, String logTag, String label, ResultConfig resultConfig, ResultData resultData, boolean logStdoutAndStderr) {
|
||||
if (context == null || resultConfig == null || resultData == null || DataUtils.isNullOrEmpty(resultConfig.resultDirectoryPath))
|
||||
return FunctionErrno.ERRNO_NULL_OR_EMPTY_PARAMETER.getError("context, resultConfig, resultData or resultConfig.resultDirectoryPath", "sendCommandResultDataToDirectory");
|
||||
|
||||
logTag = DataUtils.getDefaultIfNull(logTag, LOG_TAG);
|
||||
|
||||
Error error;
|
||||
|
||||
String resultDataStdout = resultData.stdout.toString();
|
||||
String resultDataStderr = resultData.stderr.toString();
|
||||
|
||||
String resultDataExitCode = "";
|
||||
if (resultData.exitCode != null)
|
||||
resultDataExitCode = String.valueOf(resultData.exitCode);
|
||||
|
||||
String resultDataErrmsg = null;
|
||||
if (resultData.isStateFailed()) {
|
||||
resultDataErrmsg = ResultData.getErrorsListLogString(resultData);
|
||||
}
|
||||
resultDataErrmsg = DataUtils.getDefaultIfNull(resultDataErrmsg, "");
|
||||
|
||||
resultConfig.resultDirectoryPath = FileUtils.getCanonicalPath(resultConfig.resultDirectoryPath, null);
|
||||
|
||||
Logger.logDebugExtended(logTag, "Writing result for command \"" + label + "\":\n" + resultConfig.toString() + "\n" + ResultData.getResultDataLogString(resultData, logStdoutAndStderr));
|
||||
|
||||
// If resultDirectoryPath is not a directory, or is not readable or writable, then just return
|
||||
// Creation of missing directory and setting of read, write and execute permissions are
|
||||
// only done if resultDirectoryPath is under resultDirectoryAllowedParentPath.
|
||||
// We try to set execute permissions, but ignore if they are missing, since only read and write
|
||||
// permissions are required for working directories.
|
||||
error = FileUtils.validateDirectoryFileExistenceAndPermissions("result", resultConfig.resultDirectoryPath,
|
||||
resultConfig.resultDirectoryAllowedParentPath, true,
|
||||
FileUtils.APP_WORKING_DIRECTORY_PERMISSIONS, true, true,
|
||||
true, true);
|
||||
if (error != null) {
|
||||
error.appendMessage("\n" + context.getString(R.string.msg_directory_absolute_path, "Result", resultConfig.resultDirectoryPath));
|
||||
return error;
|
||||
}
|
||||
|
||||
if (resultConfig.resultSingleFile) {
|
||||
// If resultFileBasename is null, empty or contains forward slashes "/"
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileBasename) ||
|
||||
resultConfig.resultFileBasename.contains("/")) {
|
||||
error = ResultSenderErrno.ERROR_RESULT_FILE_BASENAME_NULL_OR_INVALID.getError(resultConfig.resultFileBasename);
|
||||
return error;
|
||||
}
|
||||
|
||||
String error_or_output;
|
||||
|
||||
if (resultData.isStateFailed()) {
|
||||
try {
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileErrorFormat)) {
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_FAILED_ERR__ERRMSG__STDOUT__STDERR__EXIT_CODE,
|
||||
MarkdownUtils.getMarkdownCodeForString(String.valueOf(resultData.getErrCode()), false),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataErrmsg, true),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataStdout, true),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataStderr, true),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
|
||||
} else {
|
||||
error_or_output = String.format(resultConfig.resultFileErrorFormat,
|
||||
resultData.getErrCode(), resultDataErrmsg, resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = ResultSenderErrno.ERROR_FORMAT_RESULT_ERROR_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||
return error;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (DataUtils.isNullOrEmpty(resultConfig.resultFileOutputFormat)) {
|
||||
if (resultDataStderr.isEmpty() && resultDataExitCode.equals("0"))
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT, resultDataStdout);
|
||||
else if (resultDataStderr.isEmpty())
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__EXIT_CODE,
|
||||
resultDataStdout,
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
|
||||
else
|
||||
error_or_output = String.format(RESULT_SENDER.FORMAT_SUCCESS_STDOUT__STDERR__EXIT_CODE,
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataStdout, true),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataStderr, true),
|
||||
MarkdownUtils.getMarkdownCodeForString(resultDataExitCode, false));
|
||||
} else {
|
||||
error_or_output = String.format(resultConfig.resultFileOutputFormat,
|
||||
resultDataStdout, resultDataStderr, resultDataExitCode);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
error = ResultSenderErrno.ERROR_FORMAT_RESULT_OUTPUT_FAILED_WITH_EXCEPTION.getError(e.getMessage());
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write error or output to temp file
|
||||
// Check errCode file creation below for explanation for why temp file is used
|
||||
String temp_filename = resultConfig.resultFileBasename + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
null, error_or_output, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Move error or output temp file to final destination
|
||||
error = FileUtils.moveRegularFile("error or output temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
resultConfig.resultDirectoryPath + "/" + resultConfig.resultFileBasename, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
} else {
|
||||
String filename;
|
||||
|
||||
// Default to no suffix, useful if user expects result in an empty directory, like created with mktemp
|
||||
if (resultConfig.resultFilesSuffix == null)
|
||||
resultConfig.resultFilesSuffix = "";
|
||||
|
||||
// If resultFilesSuffix contains forward slashes "/"
|
||||
if (resultConfig.resultFilesSuffix.contains("/")) {
|
||||
error = ResultSenderErrno.ERROR_RESULT_FILES_SUFFIX_INVALID.getError(resultConfig.resultFilesSuffix);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Write result to result files under resultDirectoryPath
|
||||
|
||||
// Write stdout to file
|
||||
if (!resultDataStdout.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_STDOUT_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataStdout, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write stderr to file
|
||||
if (!resultDataStderr.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_STDERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataStderr, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write exitCode to file
|
||||
if (!resultDataExitCode.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_EXIT_CODE_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataExitCode, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write errmsg to file
|
||||
if (resultData.isStateFailed() && !resultDataErrmsg.isEmpty()) {
|
||||
filename = RESULT_SENDER.RESULT_FILE_ERRMSG_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(filename, resultConfig.resultDirectoryPath + "/" + filename,
|
||||
null, resultDataErrmsg, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
// Write errCode to file
|
||||
// This must be created after writing to other result files has already finished since
|
||||
// caller should wait for this file to be created to be notified that the command has
|
||||
// finished and should then start reading from the rest of the result files if they exist.
|
||||
// Since there may be a delay between creation of errCode file and writing to it or flushing
|
||||
// to disk, we create a temp file first and then move it to the final destination, since
|
||||
// caller may otherwise read from an empty file in some cases.
|
||||
|
||||
// Write errCode to temp file
|
||||
String temp_filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + "-" + AndroidUtils.getCurrentMilliSecondLocalTimeStamp();
|
||||
if (!resultConfig.resultFilesSuffix.isEmpty()) temp_filename = temp_filename + "-" + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.writeStringToFile(temp_filename, resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
null, String.valueOf(resultData.getErrCode()), false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// Move errCode temp file to final destination
|
||||
filename = RESULT_SENDER.RESULT_FILE_ERR_PREFIX + resultConfig.resultFilesSuffix;
|
||||
error = FileUtils.moveRegularFile(RESULT_SENDER.RESULT_FILE_ERR_PREFIX + " temp file", resultConfig.resultDirectoryPath + "/" + temp_filename,
|
||||
resultConfig.resultDirectoryPath + "/" + filename, false);
|
||||
if (error != null) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface ShellEnvironmentClient {
|
||||
|
||||
/**
|
||||
* Get the default working directory path for the environment in case the path that was passed
|
||||
* was {@code null} or empty.
|
||||
*
|
||||
* @return Should return the default working directory path.
|
||||
*/
|
||||
@NonNull
|
||||
String getDefaultWorkingDirectoryPath();
|
||||
|
||||
/**
|
||||
* Get the default "/bin" path, likely $PREFIX/bin.
|
||||
*
|
||||
* @return Should return the "/bin" path.
|
||||
*/
|
||||
@NonNull
|
||||
String getDefaultBinPath();
|
||||
|
||||
/**
|
||||
* Build the shell environment to be used for commands.
|
||||
*
|
||||
* @param currentPackageContext The {@link Context} for the current package.
|
||||
* @param isFailSafe If running a failsafe session.
|
||||
* @param workingDirectory The working directory for the environment.
|
||||
* @return Should return the build environment.
|
||||
*/
|
||||
@NonNull
|
||||
String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory);
|
||||
|
||||
/**
|
||||
* Setup process arguments for the file to execute, like interpreter, etc.
|
||||
*
|
||||
* @param fileToExecute The file to execute.
|
||||
* @param arguments The arguments to pass to the executable.
|
||||
* @return Should return the final process arguments.
|
||||
*/
|
||||
@NonNull
|
||||
String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import com.termux.terminal.TerminalBuffer;
|
||||
import com.termux.terminal.TerminalEmulator;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public class ShellUtils {
|
||||
|
||||
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 getExecutableBasename(String executable) {
|
||||
if (executable == null) return null;
|
||||
int lastSlash = executable.lastIndexOf('/');
|
||||
return (lastSlash == -1) ? executable : executable.substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
public static String getTerminalSessionTranscriptText(TerminalSession terminalSession, boolean linesJoined, boolean trim) {
|
||||
if (terminalSession == null) return null;
|
||||
|
||||
TerminalEmulator terminalEmulator = terminalSession.getEmulator();
|
||||
if (terminalEmulator == null) return null;
|
||||
|
||||
TerminalBuffer terminalBuffer = terminalEmulator.getScreen();
|
||||
if (terminalBuffer == null) return null;
|
||||
|
||||
String transcriptText;
|
||||
|
||||
if (linesJoined)
|
||||
transcriptText = terminalBuffer.getTranscriptTextWithFullLinesJoined();
|
||||
else
|
||||
transcriptText = terminalBuffer.getTranscriptTextWithoutJoinedLines();
|
||||
|
||||
if (transcriptText == null) return null;
|
||||
|
||||
if (trim)
|
||||
transcriptText = transcriptText.trim();
|
||||
|
||||
return transcriptText;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
/*
|
||||
* 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
|
||||
*
|
||||
* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/Shell.java#L141
|
||||
* https://github.com/Chainfire/libsuperuser/blob/1.1.0.201907261845/libsuperuser/src/eu/chainfire/libsuperuser/StreamGobbler.java
|
||||
*/
|
||||
@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;
|
||||
@Nullable
|
||||
private final Integer mLogLevel;
|
||||
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
|
||||
* @param logLevel The custom log level to use for logging the command output. If set to
|
||||
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
||||
*/
|
||||
@AnyThread
|
||||
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
||||
@Nullable List<String> outputList,
|
||||
@Nullable Integer logLevel) {
|
||||
super("Gobbler#" + incThreadCounter());
|
||||
this.shell = shell;
|
||||
this.inputStream = inputStream;
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
streamClosedListener = null;
|
||||
|
||||
listWriter = outputList;
|
||||
stringWriter = null;
|
||||
lineListener = null;
|
||||
|
||||
mLogLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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
|
||||
* @param logLevel The custom log level to use for logging the command output. If set to
|
||||
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
||||
*/
|
||||
@AnyThread
|
||||
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
||||
@Nullable StringBuilder outputString,
|
||||
@Nullable Integer logLevel) {
|
||||
super("Gobbler#" + incThreadCounter());
|
||||
this.shell = shell;
|
||||
this.inputStream = inputStream;
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
streamClosedListener = null;
|
||||
|
||||
listWriter = null;
|
||||
stringWriter = outputString;
|
||||
lineListener = null;
|
||||
|
||||
mLogLevel = logLevel;
|
||||
}
|
||||
|
||||
/**
|
||||
* <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
|
||||
* @param logLevel The custom log level to use for logging the command output. If set to
|
||||
* {@code null}, then {@link Logger#LOG_LEVEL_VERBOSE} will be used.
|
||||
*/
|
||||
@AnyThread
|
||||
public StreamGobbler(@NonNull String shell, @NonNull InputStream inputStream,
|
||||
@Nullable OnLineListener onLineListener,
|
||||
@Nullable OnStreamClosedListener onStreamClosedListener,
|
||||
@Nullable Integer logLevel) {
|
||||
super("Gobbler#" + incThreadCounter());
|
||||
this.shell = shell;
|
||||
this.inputStream = inputStream;
|
||||
reader = new BufferedReader(new InputStreamReader(inputStream));
|
||||
streamClosedListener = onStreamClosedListener;
|
||||
|
||||
listWriter = null;
|
||||
stringWriter = null;
|
||||
lineListener = onLineListener;
|
||||
|
||||
mLogLevel = logLevel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
String defaultLogTag = Logger.DEFAULT_LOG_TAG;
|
||||
boolean loggingEnabled = Logger.shouldEnableLoggingForCustomLogLevel(mLogLevel);
|
||||
if (loggingEnabled)
|
||||
Logger.logVerbose(LOG_TAG, "Using custom log level: " + mLogLevel + ", current log level: " + Logger.getLogLevel());
|
||||
|
||||
// keep reading the InputStream until it ends (or an error occurs)
|
||||
// optionally pausing when a command is executed that consumes the InputStream itself
|
||||
try {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (loggingEnabled)
|
||||
Logger.logVerboseForce(defaultLogTag + "Command", 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,269 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* A class that maintains info for foreground Termux sessions.
|
||||
* It also provides a way to link each {@link TerminalSession} with the {@link ExecutionCommand}
|
||||
* that started it.
|
||||
*/
|
||||
public class TermuxSession {
|
||||
|
||||
private final TerminalSession mTerminalSession;
|
||||
private final ExecutionCommand mExecutionCommand;
|
||||
private final TermuxSessionClient mTermuxSessionClient;
|
||||
private final boolean mSetStdoutOnExit;
|
||||
|
||||
private static final String LOG_TAG = "TermuxSession";
|
||||
|
||||
private TermuxSession(@NonNull final TerminalSession terminalSession, @NonNull final ExecutionCommand executionCommand,
|
||||
final TermuxSessionClient termuxSessionClient, final boolean setStdoutOnExit) {
|
||||
this.mTerminalSession = terminalSession;
|
||||
this.mExecutionCommand = executionCommand;
|
||||
this.mTermuxSessionClient = termuxSessionClient;
|
||||
this.mSetStdoutOnExit = setStdoutOnExit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start execution of an {@link ExecutionCommand} with {@link Runtime#exec(String[], String[], File)}.
|
||||
*
|
||||
* The {@link ExecutionCommand#executable}, must be set, {@link ExecutionCommand#commandLabel},
|
||||
* {@link ExecutionCommand#arguments} and {@link ExecutionCommand#workingDirectory} may optionally
|
||||
* be set.
|
||||
*
|
||||
* If {@link ExecutionCommand#executable} is {@code null}, then a default shell is automatically
|
||||
* chosen.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
|
||||
* @param terminalSessionClient The {@link TerminalSessionClient} interface implementation.
|
||||
* @param termuxSessionClient The {@link TermuxSessionClient} interface implementation.
|
||||
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||
* @param sessionName The optional {@link TerminalSession} name.
|
||||
* @param setStdoutOnExit If set to {@code true}, then the {@link ResultData#stdout}
|
||||
* available in the {@link TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
|
||||
* callback will be set to the {@link TerminalSession} transcript. The session
|
||||
* transcript will contain both stdout and stderr combined, basically
|
||||
* anything sent to the the pseudo terminal /dev/pts, including PS1 prefixes.
|
||||
* Set this to {@code true} only if the session transcript is required,
|
||||
* since this requires extra processing to get it.
|
||||
* @return Returns the {@link TermuxSession}. This will be {@code null} if failed to start the execution command.
|
||||
*/
|
||||
public static TermuxSession execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||
@NonNull final TerminalSessionClient terminalSessionClient, final TermuxSessionClient termuxSessionClient,
|
||||
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||
final String sessionName, final boolean setStdoutOnExit) {
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||
if (executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = "/";
|
||||
|
||||
String[] environment = shellEnvironmentClient.buildEnvironment(context, executionCommand.isFailsafe, executionCommand.workingDirectory);
|
||||
|
||||
String defaultBinPath = shellEnvironmentClient.getDefaultBinPath();
|
||||
if (defaultBinPath.isEmpty())
|
||||
defaultBinPath = "/system/bin";
|
||||
|
||||
boolean isLoginShell = false;
|
||||
if (executionCommand.executable == null) {
|
||||
if (!executionCommand.isFailsafe) {
|
||||
for (String shellBinary : new String[]{"login", "bash", "zsh"}) {
|
||||
File shellFile = new File(defaultBinPath, shellBinary);
|
||||
if (shellFile.canExecute()) {
|
||||
executionCommand.executable = shellFile.getAbsolutePath();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (executionCommand.executable == null) {
|
||||
// Fall back to system shell as last resort:
|
||||
// Do not start a login shell since ~/.profile may cause startup failure if its invalid.
|
||||
// /system/bin/sh is provided by mksh (not toybox) and does load .mkshrc but for android its set
|
||||
// to /system/etc/mkshrc even though its default is ~/.mkshrc.
|
||||
// So /system/etc/mkshrc must still be valid for failsafe session to start properly.
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=663
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/src/main.c;l=41
|
||||
// https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:external/mksh/Android.bp;l=114
|
||||
executionCommand.executable = "/system/bin/sh";
|
||||
} else {
|
||||
isLoginShell = true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
String[] processArgs = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
|
||||
executionCommand.executable = processArgs[0];
|
||||
String processName = (isLoginShell ? "-" : "") + ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
String[] arguments = new String[processArgs.length];
|
||||
arguments[0] = processName;
|
||||
if (processArgs.length > 1) System.arraycopy(processArgs, 1, arguments, 1, processArgs.length - 1);
|
||||
|
||||
executionCommand.arguments = arguments;
|
||||
|
||||
if (executionCommand.commandLabel == null)
|
||||
executionCommand.commandLabel = processName;
|
||||
|
||||
if (!executionCommand.setState(ExecutionCommand.ExecutionState.EXECUTING)) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_session_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||
TermuxSession.processTermuxSessionResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
|
||||
Logger.logDebugExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Running \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
TerminalSession terminalSession = new TerminalSession(executionCommand.executable, executionCommand.workingDirectory, executionCommand.arguments, environment, executionCommand.terminalTranscriptRows, terminalSessionClient);
|
||||
|
||||
if (sessionName != null) {
|
||||
terminalSession.mSessionName = sessionName;
|
||||
}
|
||||
|
||||
return new TermuxSession(terminalSession, executionCommand, termuxSessionClient, setStdoutOnExit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal that this {@link TermuxSession} has finished. This should be called when
|
||||
* {@link TerminalSessionClient#onSessionFinished(TerminalSession)} callback is received by the caller.
|
||||
*
|
||||
* If the processes has finished, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* and then calls {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)} to process the result}.
|
||||
*
|
||||
*/
|
||||
public void finish() {
|
||||
// If process is still running, then ignore the call
|
||||
if (mTerminalSession.isRunning()) return;
|
||||
|
||||
int exitCode = mTerminalSession.getExitStatus();
|
||||
|
||||
if (exitCode == 0)
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited normally");
|
||||
else
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession exited with code: " + exitCode);
|
||||
|
||||
// If the execution command has already failed, like SIGKILL was sent, then don't continue
|
||||
if (mExecutionCommand.isStateFailed()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring setting \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession state to ExecutionState.EXECUTED and processing results since it has already failed");
|
||||
return;
|
||||
}
|
||||
|
||||
mExecutionCommand.resultData.exitCode = exitCode;
|
||||
|
||||
if (this.mSetStdoutOnExit)
|
||||
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||
|
||||
if (!mExecutionCommand.setState(ExecutionCommand.ExecutionState.EXECUTED))
|
||||
return;
|
||||
|
||||
TermuxSession.processTermuxSessionResult(this, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill this {@link TermuxSession} by sending a {@link OsConstants#SIGILL} to its {@link #mTerminalSession}
|
||||
* if its still executing.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param processResult If set to {@code true}, then the {@link #processTermuxSessionResult(TermuxSession, ExecutionCommand)}
|
||||
* will be called to process the failure.
|
||||
*/
|
||||
public void killIfExecuting(@NonNull final Context context, boolean processResult) {
|
||||
// If execution command has already finished executing, then no need to process results or send SIGKILL
|
||||
if (mExecutionCommand.hasExecuted()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring sending SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession since it has already finished executing");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession");
|
||||
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||
if (processResult) {
|
||||
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||
|
||||
// Get whatever output has been set till now in case its needed
|
||||
if (this.mSetStdoutOnExit)
|
||||
mExecutionCommand.resultData.stdout.append(ShellUtils.getTerminalSessionTranscriptText(mTerminalSession, true, false));
|
||||
|
||||
TermuxSession.processTermuxSessionResult(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Send SIGKILL to process
|
||||
mTerminalSession.finishIfRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the results of {@link TermuxSession} or {@link ExecutionCommand}.
|
||||
*
|
||||
* Only one of {@code termuxSession} and {@code executionCommand} must be set.
|
||||
*
|
||||
* If the {@code termuxSession} and its {@link #mTermuxSessionClient} are not {@code null},
|
||||
* then the {@link TermuxSession.TermuxSessionClient#onTermuxSessionExited(TermuxSession)}
|
||||
* callback will be called.
|
||||
*
|
||||
* @param termuxSession The {@link TermuxSession}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||
* successfully started the process.
|
||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TerminalSessionClient, TermuxSessionClient, ShellEnvironmentClient, String, boolean)}
|
||||
* failed to start the process.
|
||||
*/
|
||||
private static void processTermuxSessionResult(final TermuxSession termuxSession, ExecutionCommand executionCommand) {
|
||||
if (termuxSession != null)
|
||||
executionCommand = termuxSession.mExecutionCommand;
|
||||
|
||||
if (executionCommand == null) return;
|
||||
|
||||
if (executionCommand.shouldNotProcessResults()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring duplicate call to process \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession result");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Processing \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxSession result");
|
||||
|
||||
if (termuxSession != null && termuxSession.mTermuxSessionClient != null) {
|
||||
termuxSession.mTermuxSessionClient.onTermuxSessionExited(termuxSession);
|
||||
} else {
|
||||
// If a callback is not set and execution command didn't fail, then we set success state now
|
||||
// Otherwise, the callback host can set it himself when its done with the termuxSession
|
||||
if (!executionCommand.isStateFailed())
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
public TerminalSession getTerminalSession() {
|
||||
return mTerminalSession;
|
||||
}
|
||||
|
||||
public ExecutionCommand getExecutionCommand() {
|
||||
return mExecutionCommand;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface TermuxSessionClient {
|
||||
|
||||
/**
|
||||
* Callback function for when {@link TermuxSession} exits.
|
||||
*
|
||||
* @param termuxSession The {@link TermuxSession} that exited.
|
||||
*/
|
||||
void onTermuxSessionExited(TermuxSession termuxSession);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class TermuxShellEnvironmentClient implements ShellEnvironmentClient {
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getDefaultWorkingDirectoryPath() {
|
||||
return TermuxShellUtils.getDefaultWorkingDirectoryPath();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getDefaultBinPath() {
|
||||
return TermuxShellUtils.getDefaultBinPath();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||
return TermuxShellUtils.buildEnvironment(currentPackageContext, isFailSafe, workingDirectory);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String[] setupProcessArgs(@NonNull String fileToExecute, String[] arguments) {
|
||||
return TermuxShellUtils.setupProcessArgs(fileToExecute, arguments);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.android.SELinuxUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class TermuxShellUtils {
|
||||
|
||||
public static String TERMUX_VERSION_NAME;
|
||||
public static String TERMUX_IS_DEBUGGABLE_BUILD;
|
||||
public static String TERMUX_APP_PID;
|
||||
public static String TERMUX_APK_RELEASE;
|
||||
|
||||
public static String TERMUX_API_VERSION_NAME;
|
||||
|
||||
public static String getDefaultWorkingDirectoryPath() {
|
||||
return TermuxConstants.TERMUX_HOME_DIR_PATH;
|
||||
}
|
||||
|
||||
public static String getDefaultBinPath() {
|
||||
return TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH;
|
||||
}
|
||||
|
||||
public static String[] buildEnvironment(Context currentPackageContext, boolean isFailSafe, String workingDirectory) {
|
||||
TermuxConstants.TERMUX_HOME_DIR.mkdirs();
|
||||
|
||||
if (workingDirectory == null || workingDirectory.isEmpty())
|
||||
workingDirectory = getDefaultWorkingDirectoryPath();
|
||||
|
||||
List<String> environment = new ArrayList<>();
|
||||
|
||||
loadTermuxEnvVariables(currentPackageContext);
|
||||
|
||||
if (TERMUX_VERSION_NAME != null)
|
||||
environment.add("TERMUX_VERSION=" + TERMUX_VERSION_NAME);
|
||||
if (TERMUX_IS_DEBUGGABLE_BUILD != null)
|
||||
environment.add("TERMUX_IS_DEBUGGABLE_BUILD=" + TERMUX_IS_DEBUGGABLE_BUILD);
|
||||
if (TERMUX_APP_PID != null)
|
||||
environment.add("TERMUX_APP_PID=" + TERMUX_APP_PID);
|
||||
if (TERMUX_APK_RELEASE != null)
|
||||
environment.add("TERMUX_APK_RELEASE=" + TERMUX_APK_RELEASE);
|
||||
|
||||
if (TERMUX_API_VERSION_NAME != null)
|
||||
environment.add("TERMUX_API_VERSION=" + TERMUX_API_VERSION_NAME);
|
||||
|
||||
|
||||
|
||||
environment.add("TERM=xterm-256color");
|
||||
environment.add("COLORTERM=truecolor");
|
||||
|
||||
try {
|
||||
ApplicationInfo applicationInfo = currentPackageContext.getPackageManager().getApplicationInfo(
|
||||
TermuxConstants.TERMUX_PACKAGE_NAME, 0);
|
||||
if (applicationInfo != null && !applicationInfo.enabled) {
|
||||
applicationInfo = null;
|
||||
}
|
||||
|
||||
if (applicationInfo != null) {
|
||||
environment.add("TERMUX_APP__DATA_DIR=" + applicationInfo.dataDir);
|
||||
environment.add("TERMUX_APP__LEGACY_DATA_DIR=" + "/data/data/" + applicationInfo.packageName);
|
||||
|
||||
environment.add("TERMUX_APP__SE_FILE_CONTEXT=" + SELinuxUtils.getFileContext(applicationInfo.dataDir));
|
||||
|
||||
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
|
||||
environment.add("TERMUX_APP__SE_INFO=" + PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
|
||||
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
|
||||
}
|
||||
|
||||
} catch (final Exception e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
environment.add("TERMUX__ROOTFS_DIR=" + TermuxConstants.TERMUX_FILES_DIR_PATH);
|
||||
environment.add("HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("TERMUX__HOME=" + TermuxConstants.TERMUX_HOME_DIR_PATH);
|
||||
environment.add("PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
environment.add("TERMUX__PREFIX=" + TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
|
||||
environment.add("TERMUX__SE_PROCESS_CONTEXT=" + SELinuxUtils.getContext());
|
||||
|
||||
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");
|
||||
|
||||
environment.add("ANDROID__BUILD_VERSION_SDK=" + Build.VERSION.SDK_INT);
|
||||
|
||||
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 String[] setupProcessArgs(@NonNull 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 void clearTermuxTMPDIR(boolean onlyIfExists) {
|
||||
if(onlyIfExists && !FileUtils.directoryFileExists(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, false))
|
||||
return;
|
||||
|
||||
Error error;
|
||||
error = FileUtils.clearDirectory("$TMPDIR", FileUtils.getCanonicalPath(TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH, null));
|
||||
if (error != null) {
|
||||
Logger.logErrorExtended(error.toString());
|
||||
}
|
||||
}
|
||||
|
||||
public static void loadTermuxEnvVariables(Context currentPackageContext) {
|
||||
String termuxAPKReleaseOld = TERMUX_APK_RELEASE;
|
||||
TERMUX_VERSION_NAME = TERMUX_IS_DEBUGGABLE_BUILD = TERMUX_APP_PID = TERMUX_APK_RELEASE = null;
|
||||
|
||||
// Check if Termux app is installed and not disabled
|
||||
if (TermuxUtils.isTermuxAppInstalled(currentPackageContext) == null) {
|
||||
// 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) {
|
||||
TERMUX_VERSION_NAME = PackageUtils.getVersionNameForPackage(termuxPackageContext);
|
||||
TERMUX_IS_DEBUGGABLE_BUILD = PackageUtils.isAppForPackageADebuggableBuild(termuxPackageContext) ? "1" : "0";
|
||||
|
||||
TERMUX_APP_PID = TermuxUtils.getTermuxAppPID(currentPackageContext);
|
||||
|
||||
// Getting APK signature is a slightly expensive operation, so do it only when needed
|
||||
if (termuxAPKReleaseOld == null) {
|
||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(termuxPackageContext);
|
||||
if (signingCertificateSHA256Digest != null)
|
||||
TERMUX_APK_RELEASE = TermuxUtils.getAPKRelease(signingCertificateSHA256Digest).replaceAll("[^a-zA-Z]", "_").toUpperCase();
|
||||
} else {
|
||||
TERMUX_APK_RELEASE = termuxAPKReleaseOld;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TERMUX_API_VERSION_NAME = null;
|
||||
|
||||
// Check if Termux:API app is installed and not disabled
|
||||
if (TermuxUtils.isTermuxAPIAppInstalled(currentPackageContext) == null) {
|
||||
// This function may be called by a different package like a plugin, so we get version for Termux:API package via its context
|
||||
Context termuxAPIPackageContext = TermuxUtils.getTermuxAPIPackageContext(currentPackageContext);
|
||||
if (termuxAPIPackageContext != null)
|
||||
TERMUX_API_VERSION_NAME = PackageUtils.getVersionNameForPackage(termuxAPIPackageContext);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
package com.termux.shared.shell;
|
||||
|
||||
import android.content.Context;
|
||||
import android.system.ErrnoException;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.models.ResultData;
|
||||
import com.termux.shared.models.errors.Errno;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.models.ExecutionCommand.ExecutionState;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* A class that maintains info for background Termux tasks run with {@link Runtime#exec(String[], String[], File)}.
|
||||
* It also provides a way to link each {@link Process} with the {@link ExecutionCommand}
|
||||
* that started it.
|
||||
*/
|
||||
public final class TermuxTask {
|
||||
|
||||
private final Process mProcess;
|
||||
private final ExecutionCommand mExecutionCommand;
|
||||
private final TermuxTaskClient mTermuxTaskClient;
|
||||
|
||||
private static final String LOG_TAG = "TermuxTask";
|
||||
|
||||
private TermuxTask(@NonNull final Process process, @NonNull final ExecutionCommand executionCommand,
|
||||
final TermuxTaskClient termuxTaskClient) {
|
||||
this.mProcess = process;
|
||||
this.mExecutionCommand = executionCommand;
|
||||
this.mTermuxTaskClient = termuxTaskClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start execution of an {@link ExecutionCommand} with {@link Runtime#exec(String[], String[], File)}.
|
||||
*
|
||||
* The {@link ExecutionCommand#executable}, must be set.
|
||||
* The {@link ExecutionCommand#commandLabel}, {@link ExecutionCommand#arguments} and
|
||||
* {@link ExecutionCommand#workingDirectory} may optionally be set.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param executionCommand The {@link ExecutionCommand} containing the information for execution command.
|
||||
* @param termuxTaskClient The {@link TermuxTaskClient} interface implementation.
|
||||
* The {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} will
|
||||
* be called regardless of {@code isSynchronous} value but not if
|
||||
* {@code null} is returned by this method. This can
|
||||
* optionally be {@code null}.
|
||||
* @param shellEnvironmentClient The {@link ShellEnvironmentClient} interface implementation.
|
||||
* @param isSynchronous If set to {@code true}, then the command will be executed in the
|
||||
* caller thread and results returned synchronously in the {@link ExecutionCommand}
|
||||
* sub object of the {@link TermuxTask} returned.
|
||||
* If set to {@code false}, then a new thread is started run the commands
|
||||
* asynchronously in the background and control is returned to the caller thread.
|
||||
* @return Returns the {@link TermuxTask}. This will be {@code null} if failed to start the execution command.
|
||||
*/
|
||||
public static TermuxTask execute(@NonNull final Context context, @NonNull ExecutionCommand executionCommand,
|
||||
final TermuxTaskClient termuxTaskClient,
|
||||
@NonNull final ShellEnvironmentClient shellEnvironmentClient,
|
||||
final boolean isSynchronous) {
|
||||
if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = shellEnvironmentClient.getDefaultWorkingDirectoryPath();
|
||||
if (executionCommand.workingDirectory.isEmpty())
|
||||
executionCommand.workingDirectory = "/";
|
||||
|
||||
String[] env = shellEnvironmentClient.buildEnvironment(context, false, executionCommand.workingDirectory);
|
||||
|
||||
final String[] commandArray = shellEnvironmentClient.setupProcessArgs(executionCommand.executable, executionCommand.arguments);
|
||||
|
||||
if (!executionCommand.setState(ExecutionState.EXECUTING)) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()));
|
||||
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
|
||||
// No need to log stdin if logging is disabled, like for app internal scripts
|
||||
Logger.logDebugExtended(LOG_TAG, ExecutionCommand.getExecutionInputLogString(executionCommand,
|
||||
true, Logger.shouldEnableLoggingForCustomLogLevel(executionCommand.backgroundCustomLogLevel)));
|
||||
|
||||
String taskName = ShellUtils.getExecutableBasename(executionCommand.executable);
|
||||
|
||||
if (executionCommand.commandLabel == null)
|
||||
executionCommand.commandLabel = taskName;
|
||||
|
||||
// Exec the process
|
||||
final Process process;
|
||||
try {
|
||||
process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory));
|
||||
} catch (IOException e) {
|
||||
executionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_failed_to_execute_termux_task_command, executionCommand.getCommandIdAndLabelLogString()), e);
|
||||
TermuxTask.processTermuxTaskResult(null, executionCommand);
|
||||
return null;
|
||||
}
|
||||
|
||||
final TermuxTask termuxTask = new TermuxTask(process, executionCommand, termuxTaskClient);
|
||||
|
||||
if (isSynchronous) {
|
||||
try {
|
||||
termuxTask.executeInner(context);
|
||||
} catch (IllegalThreadStateException | InterruptedException e) {
|
||||
// TODO: Should either of these be handled or returned?
|
||||
}
|
||||
} else {
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
termuxTask.executeInner(context);
|
||||
} catch (IllegalThreadStateException | InterruptedException e) {
|
||||
// TODO: Should either of these be handled or returned?
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
return termuxTask;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up stdout and stderr readers for the {@link #mProcess} and waits for the process to end.
|
||||
*
|
||||
* If the processes finishes, then sets {@link ResultData#stdout}, {@link ResultData#stderr}
|
||||
* and {@link ResultData#exitCode} for the {@link #mExecutionCommand} of the {@code termuxTask}
|
||||
* and then calls {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand) to process the result}.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
*/
|
||||
private void executeInner(@NonNull final Context context) throws IllegalThreadStateException, InterruptedException {
|
||||
final int pid = ShellUtils.getPid(mProcess);
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Running \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid);
|
||||
|
||||
mExecutionCommand.resultData.exitCode = null;
|
||||
|
||||
// setup stdin, and stdout and stderr gobblers
|
||||
DataOutputStream STDIN = new DataOutputStream(mProcess.getOutputStream());
|
||||
StreamGobbler STDOUT = new StreamGobbler(pid + "-stdout", mProcess.getInputStream(), mExecutionCommand.resultData.stdout, mExecutionCommand.backgroundCustomLogLevel);
|
||||
StreamGobbler STDERR = new StreamGobbler(pid + "-stderr", mProcess.getErrorStream(), mExecutionCommand.resultData.stderr, mExecutionCommand.backgroundCustomLogLevel);
|
||||
|
||||
// start gobbling
|
||||
STDOUT.start();
|
||||
STDERR.start();
|
||||
|
||||
if (!DataUtils.isNullOrEmpty(mExecutionCommand.stdin)) {
|
||||
try {
|
||||
STDIN.write((mExecutionCommand.stdin + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
STDIN.flush();
|
||||
STDIN.close();
|
||||
//STDIN.write("exit\n".getBytes(StandardCharsets.UTF_8));
|
||||
//STDIN.flush();
|
||||
} catch(IOException e) {
|
||||
if (e.getMessage() != null && (e.getMessage().contains("EPIPE") || e.getMessage().contains("Stream closed"))) {
|
||||
// Method most horrid to catch broken pipe, in which case we
|
||||
// do nothing. The command is not a shell, the shell closed
|
||||
// STDIN, the script already contained the exit command, etc.
|
||||
// these cases we want the output instead of returning null.
|
||||
} else {
|
||||
// other issues we don't know how to handle, leads to
|
||||
// returning null
|
||||
mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_exception_received_while_executing_termux_task_command, mExecutionCommand.getCommandIdAndLabelLogString(), e.getMessage()), e);
|
||||
mExecutionCommand.resultData.exitCode = 1;
|
||||
TermuxTask.processTermuxTaskResult(this, null);
|
||||
kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wait for our process to finish, while we gobble away in the background
|
||||
int exitCode = mProcess.waitFor();
|
||||
|
||||
// make sure our threads are done gobbling
|
||||
// and the process is destroyed - while the latter shouldn't be
|
||||
// needed in theory, and may even produce warnings, in "normal" Java
|
||||
// they are required for guaranteed cleanup of resources, so lets be
|
||||
// safe and do this on Android as well
|
||||
try {
|
||||
STDIN.close();
|
||||
} catch (IOException e) {
|
||||
// might be closed already
|
||||
}
|
||||
STDOUT.join();
|
||||
STDERR.join();
|
||||
mProcess.destroy();
|
||||
|
||||
// Process result
|
||||
if (exitCode == 0)
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + " exited normally");
|
||||
else
|
||||
Logger.logDebug(LOG_TAG, "The \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + " exited with code: " + exitCode);
|
||||
|
||||
// If the execution command has already failed, like SIGKILL was sent, then don't continue
|
||||
if (mExecutionCommand.isStateFailed()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring setting \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask state to ExecutionState.EXECUTED and processing results since it has already failed");
|
||||
return;
|
||||
}
|
||||
|
||||
mExecutionCommand.resultData.exitCode = exitCode;
|
||||
|
||||
if (!mExecutionCommand.setState(ExecutionState.EXECUTED))
|
||||
return;
|
||||
|
||||
TermuxTask.processTermuxTaskResult(this, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill this {@link TermuxTask} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}
|
||||
* if its still executing.
|
||||
*
|
||||
* @param context The {@link Context} for operations.
|
||||
* @param processResult If set to {@code true}, then the {@link #processTermuxTaskResult(TermuxTask, ExecutionCommand)}
|
||||
* will be called to process the failure.
|
||||
*/
|
||||
public void killIfExecuting(@NonNull final Context context, boolean processResult) {
|
||||
// If execution command has already finished executing, then no need to process results or send SIGKILL
|
||||
if (mExecutionCommand.hasExecuted()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring sending SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask since it has already finished executing");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask");
|
||||
|
||||
if (mExecutionCommand.setStateFailed(Errno.ERRNO_FAILED.getCode(), context.getString(R.string.error_sending_sigkill_to_process))) {
|
||||
if (processResult) {
|
||||
mExecutionCommand.resultData.exitCode = 137; // SIGKILL
|
||||
TermuxTask.processTermuxTaskResult(this, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (mExecutionCommand.isExecuting()) {
|
||||
kill();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kill this {@link TermuxTask} by sending a {@link OsConstants#SIGILL} to its {@link #mProcess}.
|
||||
*/
|
||||
public void kill() {
|
||||
int pid = ShellUtils.getPid(mProcess);
|
||||
try {
|
||||
// Send SIGKILL to process
|
||||
Os.kill(pid, OsConstants.SIGKILL);
|
||||
} catch (ErrnoException e) {
|
||||
Logger.logWarn(LOG_TAG, "Failed to send SIGKILL to \"" + mExecutionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask with pid " + pid + ": " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the results of {@link TermuxTask} or {@link ExecutionCommand}.
|
||||
*
|
||||
* Only one of {@code termuxTask} and {@code executionCommand} must be set.
|
||||
*
|
||||
* If the {@code termuxTask} and its {@link #mTermuxTaskClient} are not {@code null},
|
||||
* then the {@link TermuxTaskClient#onTermuxTaskExited(TermuxTask)} callback will be called.
|
||||
*
|
||||
* @param termuxTask The {@link TermuxTask}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||
* successfully started the process.
|
||||
* @param executionCommand The {@link ExecutionCommand}, which should be set if
|
||||
* {@link #execute(Context, ExecutionCommand, TermuxTaskClient, ShellEnvironmentClient, boolean)}
|
||||
* failed to start the process.
|
||||
*/
|
||||
private static void processTermuxTaskResult(final TermuxTask termuxTask, ExecutionCommand executionCommand) {
|
||||
if (termuxTask != null)
|
||||
executionCommand = termuxTask.mExecutionCommand;
|
||||
|
||||
if (executionCommand == null) return;
|
||||
|
||||
if (executionCommand.shouldNotProcessResults()) {
|
||||
Logger.logDebug(LOG_TAG, "Ignoring duplicate call to process \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask result");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.logDebug(LOG_TAG, "Processing \"" + executionCommand.getCommandIdAndLabelLogString() + "\" TermuxTask result");
|
||||
|
||||
if (termuxTask != null && termuxTask.mTermuxTaskClient != null) {
|
||||
termuxTask.mTermuxTaskClient.onTermuxTaskExited(termuxTask);
|
||||
} else {
|
||||
// If a callback is not set and execution command didn't fail, then we set success state now
|
||||
// Otherwise, the callback host can set it himself when its done with the termuxTask
|
||||
if (!executionCommand.isStateFailed())
|
||||
executionCommand.setState(ExecutionCommand.ExecutionState.SUCCESS);
|
||||
}
|
||||
}
|
||||
|
||||
public Process getProcess() {
|
||||
return mProcess;
|
||||
}
|
||||
|
||||
public ExecutionCommand getExecutionCommand() {
|
||||
return mExecutionCommand;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public interface TermuxTaskClient {
|
||||
|
||||
/**
|
||||
* Callback function for when {@link TermuxTask} exits.
|
||||
*
|
||||
* @param termuxTask The {@link TermuxTask} that exited.
|
||||
*/
|
||||
void onTermuxTaskExited(TermuxTask termuxTask);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.termux.shared.terminal;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.terminal.TerminalSessionClient;
|
||||
|
||||
public class TermuxTerminalSessionClientBase implements TerminalSessionClient {
|
||||
|
||||
public TermuxTerminalSessionClientBase() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(TerminalSession changedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChanged(TerminalSession updatedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionFinished(final TerminalSession finishedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCopyTextToClipboard(TerminalSession session, String text) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasteTextFromClipboard(TerminalSession session) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBell(TerminalSession session) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorsChanged(TerminalSession changedSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTerminalCursorStateChange(boolean state) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public Integer getTerminalCursorStyle() {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void logError(String tag, String message) {
|
||||
Logger.logError(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logWarn(String tag, String message) {
|
||||
Logger.logWarn(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String tag, String message) {
|
||||
Logger.logInfo(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logDebug(String tag, String message) {
|
||||
Logger.logDebug(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logVerbose(String tag, String message) {
|
||||
Logger.logVerbose(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTraceWithMessage(String tag, String message, Exception e) {
|
||||
Logger.logStackTraceWithMessage(tag, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTrace(String tag, Exception e) {
|
||||
Logger.logStackTrace(tag, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package com.termux.shared.terminal;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.terminal.TerminalSession;
|
||||
import com.termux.view.TerminalViewClient;
|
||||
|
||||
public class TermuxTerminalViewClientBase implements TerminalViewClient {
|
||||
|
||||
public TermuxTerminalViewClientBase() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public float onScale(float scale) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSingleTapUp(MotionEvent e) {
|
||||
}
|
||||
|
||||
public boolean shouldBackButtonBeMappedToEscape() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean shouldEnforceCharBasedInput() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean shouldUseCtrlSpaceWorkaround() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminalViewSelected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void copyModeChanged(boolean copyMode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent e, TerminalSession session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onLongPress(MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readControlKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readAltKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readShiftKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readFnKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public boolean onCodePoint(int codePoint, boolean ctrlDown, TerminalSession session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmulatorSet() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logError(String tag, String message) {
|
||||
Logger.logError(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logWarn(String tag, String message) {
|
||||
Logger.logWarn(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logInfo(String tag, String message) {
|
||||
Logger.logInfo(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logDebug(String tag, String message) {
|
||||
Logger.logDebug(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logVerbose(String tag, String message) {
|
||||
Logger.logVerbose(tag, message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTraceWithMessage(String tag, String message, Exception e) {
|
||||
Logger.logStackTraceWithMessage(tag, message, e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logStackTrace(String tag, Exception e) {
|
||||
Logger.logStackTrace(tag, e);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
package com.termux.shared.terminal.io;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class BellHandler {
|
||||
private static BellHandler instance = null;
|
||||
private static final Object lock = new Object();
|
||||
|
||||
private static final String LOG_TAG = "BellHandler";
|
||||
|
||||
public static BellHandler getInstance(Context context) {
|
||||
if (instance == null) {
|
||||
synchronized (lock) {
|
||||
if (instance == null) {
|
||||
instance = new BellHandler((Vibrator) context.getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static final long DURATION = 50;
|
||||
private static final long MIN_PAUSE = 3 * DURATION;
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private long lastBell = 0;
|
||||
private final Runnable bellRunnable;
|
||||
|
||||
private BellHandler(final Vibrator vibrator) {
|
||||
bellRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (vibrator != null) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(DURATION, VibrationEffect.DEFAULT_AMPLITUDE));
|
||||
} else {
|
||||
vibrator.vibrate(DURATION);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Issue on samsung devices on android 8
|
||||
// java.lang.NullPointerException: Attempt to read from field 'android.os.VibrationEffect com.android.server.VibratorService$Vibration.mEffect' on a null object reference
|
||||
Logger.logStackTraceWithMessage(LOG_TAG, "Failed to run vibrator", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public synchronized void doBell() {
|
||||
long now = now();
|
||||
long timeSinceLastBell = now - lastBell;
|
||||
|
||||
if (timeSinceLastBell < 0) {
|
||||
// there is a next bell pending; don't schedule another one
|
||||
} else if (timeSinceLastBell < MIN_PAUSE) {
|
||||
// there was a bell recently, schedule the next one
|
||||
handler.postDelayed(bellRunnable, MIN_PAUSE - timeSinceLastBell);
|
||||
lastBell = lastBell + MIN_PAUSE;
|
||||
} else {
|
||||
// the last bell was long ago, do it now
|
||||
bellRunnable.run();
|
||||
lastBell = now;
|
||||
}
|
||||
}
|
||||
|
||||
private long now() {
|
||||
return SystemClock.uptimeMillis();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
package com.termux.shared.terminal.io;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeyButton;
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysView;
|
||||
import com.termux.shared.terminal.io.extrakeys.SpecialButton;
|
||||
import com.termux.view.TerminalView;
|
||||
|
||||
import static com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.PRIMARY_KEY_CODES_FOR_STRINGS;
|
||||
|
||||
|
||||
public class TerminalExtraKeys implements ExtraKeysView.IExtraKeysView {
|
||||
|
||||
private final TerminalView mTerminalView;
|
||||
|
||||
public TerminalExtraKeys(@NonNull TerminalView terminalView) {
|
||||
mTerminalView = terminalView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (buttonInfo.isMacro()) {
|
||||
String[] keys = buttonInfo.getKey().split(" ");
|
||||
boolean ctrlDown = false;
|
||||
boolean altDown = false;
|
||||
boolean shiftDown = false;
|
||||
boolean fnDown = false;
|
||||
for (String key : keys) {
|
||||
if (SpecialButton.CTRL.getKey().equals(key)) {
|
||||
ctrlDown = true;
|
||||
} else if (SpecialButton.ALT.getKey().equals(key)) {
|
||||
altDown = true;
|
||||
} else if (SpecialButton.SHIFT.getKey().equals(key)) {
|
||||
shiftDown = true;
|
||||
} else if (SpecialButton.FN.getKey().equals(key)) {
|
||||
fnDown = true;
|
||||
} else {
|
||||
onTerminalExtraKeyButtonClick(view, key, ctrlDown, altDown, shiftDown, fnDown);
|
||||
ctrlDown = false; altDown = false; shiftDown = false; fnDown = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onTerminalExtraKeyButtonClick(view, buttonInfo.getKey(), false, false, false, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onTerminalExtraKeyButtonClick(View view, String key, boolean ctrlDown, boolean altDown, boolean shiftDown, boolean fnDown) {
|
||||
if (PRIMARY_KEY_CODES_FOR_STRINGS.containsKey(key)) {
|
||||
Integer keyCode = PRIMARY_KEY_CODES_FOR_STRINGS.get(key);
|
||||
if (keyCode == null) return;
|
||||
int metaState = 0;
|
||||
if (ctrlDown) metaState |= KeyEvent.META_CTRL_ON | KeyEvent.META_CTRL_LEFT_ON;
|
||||
if (altDown) metaState |= KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON;
|
||||
if (shiftDown) metaState |= KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON;
|
||||
if (fnDown) metaState |= KeyEvent.META_FUNCTION_ON;
|
||||
|
||||
KeyEvent keyEvent = new KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState);
|
||||
mTerminalView.onKeyDown(keyCode, keyEvent);
|
||||
} else {
|
||||
// not a control char
|
||||
key.codePoints().forEach(codePoint -> {
|
||||
mTerminalView.inputCodePoint(codePoint, ctrlDown, altDown);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExtraKeyButton {
|
||||
|
||||
/** The key name for the name of the extra key if using a dict to define the extra key. {key: name, ...} */
|
||||
public static final String KEY_KEY_NAME = "key";
|
||||
|
||||
/** The key name for the macro value of the extra key if using a dict to define the extra key. {macro: value, ...} */
|
||||
public static final String KEY_MACRO = "macro";
|
||||
|
||||
/** The key name for the alternate display name of the extra key if using a dict to define the extra key. {display: name, ...} */
|
||||
public static final String KEY_DISPLAY_NAME = "display";
|
||||
|
||||
/** The key name for the nested dict to define popup extra key info if using a dict to define the extra key. {popup: {key: name, ...}, ...} */
|
||||
public static final String KEY_POPUP = "popup";
|
||||
|
||||
|
||||
/**
|
||||
* The key that will be sent to the terminal, either a control character, like defined in
|
||||
* {@link ExtraKeysConstants#PRIMARY_KEY_CODES_FOR_STRINGS} (LEFT, RIGHT, PGUP...) or some text.
|
||||
*/
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* If the key is a macro, i.e. a sequence of keys separated by space.
|
||||
*/
|
||||
private final boolean macro;
|
||||
|
||||
/**
|
||||
* The text that will be displayed on the button.
|
||||
*/
|
||||
private final String display;
|
||||
|
||||
/**
|
||||
* The {@link ExtraKeyButton} containing the information of the popup button (triggered by swipe up).
|
||||
*/
|
||||
@Nullable
|
||||
private final ExtraKeyButton popup;
|
||||
|
||||
|
||||
/**
|
||||
* Initialize a {@link ExtraKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link ExtraKeyButton}.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public ExtraKeyButton(@NonNull JSONObject config,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
this(config, null, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a {@link ExtraKeyButton}.
|
||||
*
|
||||
* @param config The {@link JSONObject} containing the info to create the {@link ExtraKeyButton}.
|
||||
* @param popup The {@link ExtraKeyButton} optional {@link #popup} button.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link #KEY_DISPLAY_NAME}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names.
|
||||
*/
|
||||
public ExtraKeyButton(@NonNull JSONObject config, @Nullable ExtraKeyButton popup,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
String keyFromConfig = getStringFromJson(config, KEY_KEY_NAME);
|
||||
String macroFromConfig = getStringFromJson(config, KEY_MACRO);
|
||||
String[] keys;
|
||||
if (keyFromConfig != null && macroFromConfig != null) {
|
||||
throw new JSONException("Both key and macro can't be set for the same key. key: \"" + keyFromConfig + "\", macro: \"" + macroFromConfig + "\"");
|
||||
} else if (keyFromConfig != null) {
|
||||
keys = new String[]{keyFromConfig};
|
||||
this.macro = false;
|
||||
} else if (macroFromConfig != null) {
|
||||
keys = macroFromConfig.split(" ");
|
||||
this.macro = true;
|
||||
} else {
|
||||
throw new JSONException("All keys have to specify either key or macro");
|
||||
}
|
||||
|
||||
for (int i = 0; i < keys.length; i++) {
|
||||
keys[i] = replaceAlias(extraKeyAliasMap, keys[i]);
|
||||
}
|
||||
|
||||
this.key = TextUtils.join(" ", keys);
|
||||
|
||||
String displayFromConfig = getStringFromJson(config, KEY_DISPLAY_NAME);
|
||||
if (displayFromConfig != null) {
|
||||
this.display = displayFromConfig;
|
||||
} else {
|
||||
this.display = Arrays.stream(keys)
|
||||
.map(key -> extraKeyDisplayMap.get(key, key))
|
||||
.collect(Collectors.joining(" "));
|
||||
}
|
||||
|
||||
this.popup = popup;
|
||||
}
|
||||
|
||||
public String getStringFromJson(@NonNull JSONObject config, @NonNull String key) {
|
||||
try {
|
||||
return config.getString(key);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get {@link #key}. */
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/** Check whether a {@link #macro} is defined or not. */
|
||||
public boolean isMacro() {
|
||||
return macro;
|
||||
}
|
||||
|
||||
/** Get {@link #display}. */
|
||||
public String getDisplay() {
|
||||
return display;
|
||||
}
|
||||
|
||||
/** Get {@link #popup}. */
|
||||
@Nullable
|
||||
public ExtraKeyButton getPopup() {
|
||||
return popup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the alias with its actual key name if found in extraKeyAliasMap.
|
||||
*/
|
||||
public static String replaceAlias(@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap, String key) {
|
||||
return extraKeyAliasMap.get(key, key);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ExtraKeysConstants {
|
||||
|
||||
/** Defines the repetitive keys that can be passed to {@link ExtraKeysView#setRepetitiveKeys(List)}. */
|
||||
public static List<String> PRIMARY_REPETITIVE_KEYS = Arrays.asList(
|
||||
"UP", "DOWN", "LEFT", "RIGHT",
|
||||
"BKSP", "DEL",
|
||||
"PGUP", "PGDN");
|
||||
|
||||
|
||||
|
||||
/** Defines the {@link KeyEvent} for common keys. */
|
||||
public static Map<String, Integer> PRIMARY_KEY_CODES_FOR_STRINGS = new HashMap<String, Integer>() {{
|
||||
put("SPACE", KeyEvent.KEYCODE_SPACE);
|
||||
put("ESC", KeyEvent.KEYCODE_ESCAPE);
|
||||
put("TAB", KeyEvent.KEYCODE_TAB);
|
||||
put("HOME", KeyEvent.KEYCODE_MOVE_HOME);
|
||||
put("END", KeyEvent.KEYCODE_MOVE_END);
|
||||
put("PGUP", KeyEvent.KEYCODE_PAGE_UP);
|
||||
put("PGDN", KeyEvent.KEYCODE_PAGE_DOWN);
|
||||
put("INS", KeyEvent.KEYCODE_INSERT);
|
||||
put("DEL", KeyEvent.KEYCODE_FORWARD_DEL);
|
||||
put("BKSP", KeyEvent.KEYCODE_DEL);
|
||||
put("UP", KeyEvent.KEYCODE_DPAD_UP);
|
||||
put("LEFT", KeyEvent.KEYCODE_DPAD_LEFT);
|
||||
put("RIGHT", KeyEvent.KEYCODE_DPAD_RIGHT);
|
||||
put("DOWN", KeyEvent.KEYCODE_DPAD_DOWN);
|
||||
put("ENTER", KeyEvent.KEYCODE_ENTER);
|
||||
put("F1", KeyEvent.KEYCODE_F1);
|
||||
put("F2", KeyEvent.KEYCODE_F2);
|
||||
put("F3", KeyEvent.KEYCODE_F3);
|
||||
put("F4", KeyEvent.KEYCODE_F4);
|
||||
put("F5", KeyEvent.KEYCODE_F5);
|
||||
put("F6", KeyEvent.KEYCODE_F6);
|
||||
put("F7", KeyEvent.KEYCODE_F7);
|
||||
put("F8", KeyEvent.KEYCODE_F8);
|
||||
put("F9", KeyEvent.KEYCODE_F9);
|
||||
put("F10", KeyEvent.KEYCODE_F10);
|
||||
put("F11", KeyEvent.KEYCODE_F11);
|
||||
put("F12", KeyEvent.KEYCODE_F12);
|
||||
}};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* HashMap that implements Python dict.get(key, default) function.
|
||||
* Default java.util .get(key) is then the same as .get(key, null);
|
||||
*/
|
||||
static class CleverMap<K,V> extends HashMap<K,V> {
|
||||
V get(K key, V defaultValue) {
|
||||
if (containsKey(key))
|
||||
return get(key);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExtraKeyDisplayMap extends CleverMap<String, String> {}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* Multiple maps are available to quickly change
|
||||
* the style of the keys.
|
||||
*/
|
||||
|
||||
public static class EXTRA_KEY_DISPLAY_MAPS {
|
||||
/**
|
||||
* Keys are displayed in a natural looking way, like "→" for "RIGHT"
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap CLASSIC_ARROWS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// classic arrow keys (for ◀ ▶ ▲ ▼ @see arrowVariationDisplay)
|
||||
put("LEFT", "←"); // U+2190 ← LEFTWARDS ARROW
|
||||
put("RIGHT", "→"); // U+2192 → RIGHTWARDS ARROW
|
||||
put("UP", "↑"); // U+2191 ↑ UPWARDS ARROW
|
||||
put("DOWN", "↓"); // U+2193 ↓ DOWNWARDS ARROW
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap WELL_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// well known characters // https://en.wikipedia.org/wiki/{Enter_key, Tab_key, Delete_key}
|
||||
put("ENTER", "↲"); // U+21B2 ↲ DOWNWARDS ARROW WITH TIP LEFTWARDS
|
||||
put("TAB", "↹"); // U+21B9 ↹ LEFTWARDS ARROW TO BAR OVER RIGHTWARDS ARROW TO BAR
|
||||
put("BKSP", "⌫"); // U+232B ⌫ ERASE TO THE LEFT sometimes seen and easy to understand
|
||||
put("DEL", "⌦"); // U+2326 ⌦ ERASE TO THE RIGHT not well known but easy to understand
|
||||
put("DRAWER", "☰"); // U+2630 ☰ TRIGRAM FOR HEAVEN not well known but easy to understand
|
||||
put("KEYBOARD", "⌨"); // U+2328 ⌨ KEYBOARD not well known but easy to understand
|
||||
put("PASTE", "⎘"); // U+2398
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap LESS_KNOWN_CHARACTERS_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// https://en.wikipedia.org/wiki/{Home_key, End_key, Page_Up_and_Page_Down_keys}
|
||||
// home key can mean "goto the beginning of line" or "goto first page" depending on context, hence the diagonal
|
||||
put("HOME", "⇱"); // from IEC 9995 // U+21F1 ⇱ NORTH WEST ARROW TO CORNER
|
||||
put("END", "⇲"); // from IEC 9995 // ⇲ // U+21F2 ⇲ SOUTH EAST ARROW TO CORNER
|
||||
put("PGUP", "⇑"); // no ISO character exists, U+21D1 ⇑ UPWARDS DOUBLE ARROW will do the trick
|
||||
put("PGDN", "⇓"); // no ISO character exists, U+21D3 ⇓ DOWNWARDS DOUBLE ARROW will do the trick
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap ARROW_TRIANGLE_VARIATION_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// alternative to classic arrow keys
|
||||
put("LEFT", "◀"); // U+25C0 ◀ BLACK LEFT-POINTING TRIANGLE
|
||||
put("RIGHT", "▶"); // U+25B6 ▶ BLACK RIGHT-POINTING TRIANGLE
|
||||
put("UP", "▲"); // U+25B2 ▲ BLACK UP-POINTING TRIANGLE
|
||||
put("DOWN", "▼"); // U+25BC ▼ BLACK DOWN-POINTING TRIANGLE
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap NOT_KNOWN_ISO_CHARACTERS = new ExtraKeyDisplayMap() {{
|
||||
// Control chars that are more clear as text // https://en.wikipedia.org/wiki/{Function_key, Alt_key, Control_key, Esc_key}
|
||||
// put("FN", "FN"); // no ISO character exists
|
||||
put("CTRL", "⎈"); // ISO character "U+2388 ⎈ HELM SYMBOL" is unknown to people and never printed on computers, however "U+25C7 ◇ WHITE DIAMOND" is a nice presentation, and "^" for terminal app and mac is often used
|
||||
put("ALT", "⎇"); // ISO character "U+2387 ⎇ ALTERNATIVE KEY SYMBOL'" is unknown to people and only printed as the Option key "⌥" on Mac computer
|
||||
put("ESC", "⎋"); // ISO character "U+238B ⎋ BROKEN CIRCLE WITH NORTHWEST ARROW" is unknown to people and not often printed on computers
|
||||
}};
|
||||
|
||||
public static final ExtraKeyDisplayMap NICER_LOOKING_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
// nicer looking for most cases
|
||||
put("-", "―"); // U+2015 ― HORIZONTAL BAR
|
||||
}};
|
||||
|
||||
/**
|
||||
* Full Iso
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap FULL_ISO_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
putAll(NOT_KNOWN_ISO_CHARACTERS); // NEW
|
||||
}};
|
||||
|
||||
/**
|
||||
* Only arrows
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap ARROWS_ONLY_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
// putAll(wellKnownCharactersDisplay); // REMOVED
|
||||
// putAll(lessKnownCharactersDisplay); // REMOVED
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Classic symbols and less known symbols
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap LOTS_OF_ARROWS_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(LESS_KNOWN_CHARACTERS_DISPLAY); // NEW
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
}};
|
||||
|
||||
/**
|
||||
* Some classic symbols everybody knows
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap DEFAULT_CHAR_DISPLAY = new ExtraKeyDisplayMap() {{
|
||||
putAll(CLASSIC_ARROWS_DISPLAY);
|
||||
putAll(WELL_KNOWN_CHARACTERS_DISPLAY);
|
||||
putAll(NICER_LOOKING_DISPLAY);
|
||||
// all other characters are displayed as themselves
|
||||
}};
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Aliases for the keys
|
||||
*/
|
||||
public static final ExtraKeyDisplayMap CONTROL_CHARS_ALIASES = new ExtraKeyDisplayMap() {{
|
||||
put("ESCAPE", "ESC");
|
||||
put("CONTROL", "CTRL");
|
||||
put("SHFT", "SHIFT");
|
||||
put("RETURN", "ENTER"); // Technically different keys, but most applications won't see the difference
|
||||
put("FUNCTION", "FN");
|
||||
// no alias for ALT
|
||||
|
||||
// Directions are sometimes written as first and last letter for brevety
|
||||
put("LT", "LEFT");
|
||||
put("RT", "RIGHT");
|
||||
put("DN", "DOWN");
|
||||
// put("UP", "UP"); well, "UP" is already two letters
|
||||
|
||||
put("PAGEUP", "PGUP");
|
||||
put("PAGE_UP", "PGUP");
|
||||
put("PAGE UP", "PGUP");
|
||||
put("PAGE-UP", "PGUP");
|
||||
|
||||
// no alias for HOME
|
||||
// no alias for END
|
||||
|
||||
put("PAGEDOWN", "PGDN");
|
||||
put("PAGE_DOWN", "PGDN");
|
||||
put("PAGE-DOWN", "PGDN");
|
||||
|
||||
put("DELETE", "DEL");
|
||||
put("BACKSPACE", "BKSP");
|
||||
|
||||
// easier for writing in termux.properties
|
||||
put("BACKSLASH", "\\");
|
||||
put("QUOTE", "\"");
|
||||
put("APOSTROPHE", "'");
|
||||
}};
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.terminal.io.extrakeys.ExtraKeysConstants.EXTRA_KEY_DISPLAY_MAPS;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* A {@link Class} that defines the info needed by {@link ExtraKeysView} to display the extra key
|
||||
* views.
|
||||
*
|
||||
* The {@code propertiesInfo} passed to the constructors of this class must be json array of arrays.
|
||||
* Each array element of the json array will be considered a separate row of keys.
|
||||
* Each key can either be simple string that defines the name of the key or a json dict that defines
|
||||
* advance info for the key. The syntax can be `'KEY'` or `{key: 'KEY'}`.
|
||||
* For example `HOME` or `{key: 'HOME', ...}.
|
||||
*
|
||||
* In advance json dict mode, the key can also be a sequence of space separated keys instead of one
|
||||
* key. This can be done by replacing `key` key/value pair of the dict with a `macro` key/value pair.
|
||||
* The syntax is `{macro: 'KEY COMBINATION'}`. For example {macro: 'HOME RIGHT', ...}.
|
||||
*
|
||||
* In advance json dict mode, you can define a nested json dict with the `popup` key which will be
|
||||
* used as the popup key and will be triggered on swipe up. The syntax can be
|
||||
* `{key: 'KEY', popup: 'POPUP_KEY'}` or `{key: 'KEY', popup: {macro: 'KEY COMBINATION', display: 'Key combo'}}`.
|
||||
* For example `{key: 'HOME', popup: {KEY: 'END', ...}, ...}`.
|
||||
*
|
||||
* In advance json dict mode, the key can also have a custom display name that can be used as the
|
||||
* text to display on the button by defining the `display` key. The syntax is `{display: 'DISPLAY'}`.
|
||||
* For example {display: 'Custom name', ...}.
|
||||
*
|
||||
* Examples:
|
||||
* {@code
|
||||
* # Empty:
|
||||
* []
|
||||
*
|
||||
* # Single row:
|
||||
* [[ESC, TAB, CTRL, ALT, {key: '-', popup: '|'}, DOWN, UP]]
|
||||
*
|
||||
* # 2 row:
|
||||
* [['ESC','/',{key: '-', popup: '|'},'HOME','UP','END','PGUP'],
|
||||
* ['TAB','CTRL','ALT','LEFT','DOWN','RIGHT','PGDN']]
|
||||
*
|
||||
* # Advance:
|
||||
* [[
|
||||
* {key: ESC, popup: {macro: "CTRL f d", display: "tmux exit"}},
|
||||
* {key: CTRL, popup: {macro: "CTRL f BKSP", display: "tmux ←"}},
|
||||
* {key: ALT, popup: {macro: "CTRL f TAB", display: "tmux →"}},
|
||||
* {key: TAB, popup: {macro: "ALT a", display: A-a}},
|
||||
* {key: LEFT, popup: HOME},
|
||||
* {key: DOWN, popup: PGDN},
|
||||
* {key: UP, popup: PGUP},
|
||||
* {key: RIGHT, popup: END},
|
||||
* {macro: "ALT j", display: A-j, popup: {macro: "ALT g", display: A-g}},
|
||||
* {key: KEYBOARD, popup: {macro: "CTRL d", display: exit}}
|
||||
* ]]
|
||||
*
|
||||
* }
|
||||
*
|
||||
* Aliases are also allowed for the keys that you can pass as {@code extraKeyAliasMap}. Check
|
||||
* {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*
|
||||
* Its up to the {@link ExtraKeysView.IExtraKeysView} client on how to handle individual key values
|
||||
* of an {@link ExtraKeyButton}. They are sent as is via
|
||||
* {@link ExtraKeysView.IExtraKeysView#onExtraKeyButtonClick(View, ExtraKeyButton, Button)}. The
|
||||
* {@link com.termux.shared.terminal.io.TerminalExtraKeys} which is an implementation of the interface,
|
||||
* checks if the key is one of {@link ExtraKeysConstants#PRIMARY_KEY_CODES_FOR_STRINGS} and generates
|
||||
* a {@link android.view.KeyEvent} for it, and if its not, then converts the key to code points by
|
||||
* calling {@link CharSequence#codePoints()} and passes them to the terminal as literal strings.
|
||||
*
|
||||
* Examples:
|
||||
* {@code
|
||||
* "ENTER" will trigger the ENTER keycode
|
||||
* "LEFT" will trigger the LEFT keycode and be displayed as "←"
|
||||
* "→" will input a "→" character
|
||||
* "−" will input a "−" character
|
||||
* "-_-" will input the string "-_-"
|
||||
* }
|
||||
*
|
||||
* For more info, check https://wiki.termux.com/wiki/Touch_Keyboard.
|
||||
*/
|
||||
public class ExtraKeysInfo {
|
||||
|
||||
/**
|
||||
* Matrix of buttons to be displayed in {@link ExtraKeysView}.
|
||||
*/
|
||||
private final ExtraKeyButton[][] mButtons;
|
||||
|
||||
/**
|
||||
* Initialize {@link ExtraKeysInfo}.
|
||||
*
|
||||
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
|
||||
* Check the class javadoc for details.
|
||||
* @param style The style to pass to {@link #getCharDisplayMapForStyle(String)} to get the
|
||||
* {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the display text
|
||||
* mapping for the keys if a custom value is not defined by
|
||||
* {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names. You can create your own or
|
||||
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*/
|
||||
public ExtraKeysInfo(@NonNull String propertiesInfo, String style,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
mButtons = initExtraKeysInfo(propertiesInfo, getCharDisplayMapForStyle(style), extraKeyAliasMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize {@link ExtraKeysInfo}.
|
||||
*
|
||||
* @param propertiesInfo The {@link String} containing the info to create the {@link ExtraKeysInfo}.
|
||||
* Check the class javadoc for details.
|
||||
* @param extraKeyDisplayMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* display text mapping for the keys if a custom value is not defined
|
||||
* by {@link ExtraKeyButton#KEY_DISPLAY_NAME} for a key. You can create
|
||||
* your own or optionally pass one of the values defined in
|
||||
* {@link #getCharDisplayMapForStyle(String)}.
|
||||
* @param extraKeyAliasMap The {@link ExtraKeysConstants.ExtraKeyDisplayMap} that defines the
|
||||
* aliases for the actual key names. You can create your own or
|
||||
* optionally pass {@link ExtraKeysConstants#CONTROL_CHARS_ALIASES}.
|
||||
*/
|
||||
public ExtraKeysInfo(@NonNull String propertiesInfo,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
mButtons = initExtraKeysInfo(propertiesInfo, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
private ExtraKeyButton[][] initExtraKeysInfo(@NonNull String propertiesInfo,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyDisplayMap,
|
||||
@NonNull ExtraKeysConstants.ExtraKeyDisplayMap extraKeyAliasMap) throws JSONException {
|
||||
// Convert String propertiesInfo to Array of Arrays
|
||||
JSONArray arr = new JSONArray(propertiesInfo);
|
||||
Object[][] matrix = new Object[arr.length()][];
|
||||
for (int i = 0; i < arr.length(); i++) {
|
||||
JSONArray line = arr.getJSONArray(i);
|
||||
matrix[i] = new Object[line.length()];
|
||||
for (int j = 0; j < line.length(); j++) {
|
||||
matrix[i][j] = line.get(j);
|
||||
}
|
||||
}
|
||||
|
||||
// convert matrix to buttons
|
||||
ExtraKeyButton[][] buttons = new ExtraKeyButton[matrix.length][];
|
||||
for (int i = 0; i < matrix.length; i++) {
|
||||
buttons[i] = new ExtraKeyButton[matrix[i].length];
|
||||
for (int j = 0; j < matrix[i].length; j++) {
|
||||
Object key = matrix[i][j];
|
||||
|
||||
JSONObject jobject = normalizeKeyConfig(key);
|
||||
|
||||
ExtraKeyButton button;
|
||||
|
||||
if (!jobject.has(ExtraKeyButton.KEY_POPUP)) {
|
||||
// no popup
|
||||
button = new ExtraKeyButton(jobject, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
} else {
|
||||
// a popup
|
||||
JSONObject popupJobject = normalizeKeyConfig(jobject.get(ExtraKeyButton.KEY_POPUP));
|
||||
ExtraKeyButton popup = new ExtraKeyButton(popupJobject, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
button = new ExtraKeyButton(jobject, popup, extraKeyDisplayMap, extraKeyAliasMap);
|
||||
}
|
||||
|
||||
buttons[i][j] = button;
|
||||
}
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert "value" -> {"key": "value"}. Required by
|
||||
* {@link ExtraKeyButton#ExtraKeyButton(JSONObject, ExtraKeyButton, ExtraKeysConstants.ExtraKeyDisplayMap, ExtraKeysConstants.ExtraKeyDisplayMap)}.
|
||||
*/
|
||||
private static JSONObject normalizeKeyConfig(Object key) throws JSONException {
|
||||
JSONObject jobject;
|
||||
if (key instanceof String) {
|
||||
jobject = new JSONObject();
|
||||
jobject.put(ExtraKeyButton.KEY_KEY_NAME, key);
|
||||
} else if (key instanceof JSONObject) {
|
||||
jobject = (JSONObject) key;
|
||||
} else {
|
||||
throw new JSONException("An key in the extra-key matrix must be a string or an object");
|
||||
}
|
||||
return jobject;
|
||||
}
|
||||
|
||||
public ExtraKeyButton[][] getMatrix() {
|
||||
return mButtons;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static ExtraKeysConstants.ExtraKeyDisplayMap getCharDisplayMapForStyle(String style) {
|
||||
switch (style) {
|
||||
case "arrows-only":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.ARROWS_ONLY_CHAR_DISPLAY;
|
||||
case "arrows-all":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.LOTS_OF_ARROWS_CHAR_DISPLAY;
|
||||
case "all":
|
||||
return EXTRA_KEY_DISPLAY_MAPS.FULL_ISO_CHAR_DISPLAY;
|
||||
case "none":
|
||||
return new ExtraKeysConstants.ExtraKeyDisplayMap();
|
||||
default:
|
||||
return EXTRA_KEY_DISPLAY_MAPS.DEFAULT_CHAR_DISPLAY;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,656 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.HashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link View} showing extra keys (such as Escape, Ctrl, Alt) not normally available on an Android soft
|
||||
* keyboards.
|
||||
*
|
||||
* To use it, add following to a layout file and import it in your activity layout file or inflate
|
||||
* it with a {@link androidx.viewpager.widget.ViewPager}.:
|
||||
* {@code
|
||||
* <?xml version="1.0" encoding="utf-8"?>
|
||||
* <com.termux.shared.terminal.io.extrakeys.ExtraKeysView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
* android:id="@+id/extra_keys"
|
||||
* style="?android:attr/buttonBarStyle"
|
||||
* android:layout_width="match_parent"
|
||||
* android:layout_height="match_parent"
|
||||
* android:layout_alignParentBottom="true"
|
||||
* android:orientation="horizontal" />
|
||||
* }
|
||||
*
|
||||
* Then in your activity, get its reference by a call to {@link android.app.Activity#findViewById(int)}
|
||||
* or {@link LayoutInflater#inflate(int, ViewGroup)} if using {@link androidx.viewpager.widget.ViewPager}.
|
||||
* Then call {@link #setExtraKeysViewClient(IExtraKeysView)} and pass it the implementation of
|
||||
* {@link IExtraKeysView} so that you can receive callbacks. You can also override other values set
|
||||
* in {@link ExtraKeysView#ExtraKeysView(Context, AttributeSet)} by calling the respective functions.
|
||||
* If you extend {@link ExtraKeysView}, you can also set them in the constructor, but do call super().
|
||||
*
|
||||
* After this you will have to make a call to {@link ExtraKeysView#reload(ExtraKeysInfo) and pass
|
||||
* it the {@link ExtraKeysInfo} to load and display the extra keys. Read its class javadocs for more
|
||||
* info on how to create it.
|
||||
*
|
||||
* Termux app defines the view in res/layout/view_terminal_toolbar_extra_keys and
|
||||
* inflates it in TerminalToolbarViewPager.instantiateItem() and sets the {@link ExtraKeysView} client
|
||||
* and calls {@link ExtraKeysView#reload(ExtraKeysInfo).
|
||||
* The {@link ExtraKeysInfo} is created by TermuxAppSharedProperties.setExtraKeys().
|
||||
* Then its got and the view height is adjusted in TermuxActivity.setTerminalToolbarHeight().
|
||||
* The client used is TermuxTerminalExtraKeys, which extends
|
||||
* {@link com.termux.shared.terminal.io.TerminalExtraKeys} to handle Termux app specific logic and
|
||||
* leave the rest to the super class.
|
||||
*/
|
||||
public final class ExtraKeysView extends GridLayout {
|
||||
|
||||
/** The client for the {@link ExtraKeysView}. */
|
||||
public interface IExtraKeysView {
|
||||
|
||||
/**
|
||||
* This is called by {@link ExtraKeysView} when a button is clicked. This is also called
|
||||
* for {@link #mRepetitiveKeys} and {@link ExtraKeyButton} that have a popup set.
|
||||
* However, this is not called for {@link #mSpecialButtons}, whose state can instead be read
|
||||
* via a call to {@link #readSpecialButton(SpecialButton, boolean)}.
|
||||
*
|
||||
* @param view The view that was clicked.
|
||||
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
|
||||
* The button may be a {@link ExtraKeyButton#KEY_MACRO} set which can be
|
||||
* checked with a call to {@link ExtraKeyButton#isMacro()}.
|
||||
* @param button The {@link Button} that was clicked.
|
||||
*/
|
||||
void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button);
|
||||
|
||||
/**
|
||||
* This is called by {@link ExtraKeysView} when a button is clicked so that the client
|
||||
* can perform any hepatic feedback. This is only called in the {@link Button.OnClickListener}
|
||||
* and not for every repeat. Its also called for {@link #mSpecialButtons}.
|
||||
*
|
||||
* @param view The view that was clicked.
|
||||
* @param buttonInfo The {@link ExtraKeyButton} for the button that was clicked.
|
||||
* @param button The {@link Button} that was clicked.
|
||||
* @return Return {@code true} if the client handled the feedback, otherwise {@code false}
|
||||
* so that {@link ExtraKeysView#performExtraKeyButtonHapticFeedback(View, ExtraKeyButton, Button)}
|
||||
* can handle it depending on system settings.
|
||||
*/
|
||||
boolean performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Defines the default value for {@link #mButtonTextColor}. */
|
||||
public static final int DEFAULT_BUTTON_TEXT_COLOR = 0xFFFFFFFF;
|
||||
/** Defines the default value for {@link #mButtonActiveTextColor}. */
|
||||
public static final int DEFAULT_BUTTON_ACTIVE_TEXT_COLOR = 0xFF80DEEA;
|
||||
/** Defines the default value for {@link #mButtonBackgroundColor}. */
|
||||
public static final int DEFAULT_BUTTON_BACKGROUND_COLOR = 0x00000000;
|
||||
/** Defines the default value for {@link #mButtonActiveBackgroundColor}. */
|
||||
public static final int DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR = 0xFF7F7F7F;
|
||||
|
||||
|
||||
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int MIN_LONG_PRESS_DURATION = 200;
|
||||
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int MAX_LONG_PRESS_DURATION = 3000;
|
||||
/** Defines the fallback duration in milliseconds for {@link #mLongPressTimeout}. */
|
||||
public static final int FALLBACK_LONG_PRESS_DURATION = 400;
|
||||
|
||||
/** Defines the minimum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int MIN_LONG_PRESS__REPEAT_DELAY = 5;
|
||||
/** Defines the maximum allowed duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int MAX_LONG_PRESS__REPEAT_DELAY = 2000;
|
||||
/** Defines the default duration in milliseconds for {@link #mLongPressRepeatDelay}. */
|
||||
public static final int DEFAULT_LONG_PRESS_REPEAT_DELAY = 80;
|
||||
|
||||
|
||||
|
||||
/** The implementation of the {@link IExtraKeysView} that acts as a client for the {@link ExtraKeysView}. */
|
||||
private IExtraKeysView mExtraKeysViewClient;
|
||||
|
||||
/** The map for the {@link SpecialButton} and their {@link SpecialButtonState}. Defaults to
|
||||
* the one returned by {@link #getDefaultSpecialButtons(ExtraKeysView)}. */
|
||||
private Map<SpecialButton, SpecialButtonState> mSpecialButtons;
|
||||
|
||||
/** The keys for the {@link SpecialButton} added to {@link #mSpecialButtons}. This is automatically
|
||||
* set when the call to {@link #setSpecialButtons(Map)} is made. */
|
||||
private Set<String> mSpecialButtonsKeys;
|
||||
|
||||
|
||||
/**
|
||||
* The list of keys for which auto repeat of key should be triggered if its extra keys button
|
||||
* is long pressed. This is done by calling {@link IExtraKeysView#onExtraKeyButtonClick(View, ExtraKeyButton, Button)}
|
||||
* every {@link #mLongPressRepeatDelay} seconds after {@link #mLongPressTimeout} has passed.
|
||||
* The default keys are defined by {@link ExtraKeysConstants#PRIMARY_REPETITIVE_KEYS}.
|
||||
*/
|
||||
private List<String> mRepetitiveKeys;
|
||||
|
||||
|
||||
/** The text color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_TEXT_COLOR}. */
|
||||
private int mButtonTextColor;
|
||||
/** The text color for the extra keys button when its active.
|
||||
* Defaults to {@link #DEFAULT_BUTTON_ACTIVE_TEXT_COLOR}. */
|
||||
private int mButtonActiveTextColor;
|
||||
/** The background color for the extra keys button. Defaults to {@link #DEFAULT_BUTTON_BACKGROUND_COLOR}. */
|
||||
private int mButtonBackgroundColor;
|
||||
/** The background color for the extra keys button when its active. Defaults to
|
||||
* {@link #DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR}. */
|
||||
private int mButtonActiveBackgroundColor;
|
||||
|
||||
/** Defines whether text for the extra keys button should be all capitalized automatically. */
|
||||
private boolean mButtonTextAllCaps = true;
|
||||
|
||||
|
||||
/**
|
||||
* Defines the duration in milliseconds before a press turns into a long press. The default
|
||||
* duration used is the one returned by a call to {@link ViewConfiguration#getLongPressTimeout()}
|
||||
* which will return the system defined duration which can be changed in accessibility settings.
|
||||
* The duration must be in between {@link #MIN_LONG_PRESS_DURATION} and {@link #MAX_LONG_PRESS_DURATION},
|
||||
* otherwise {@link #FALLBACK_LONG_PRESS_DURATION} is used.
|
||||
*/
|
||||
private int mLongPressTimeout;
|
||||
|
||||
/**
|
||||
* Defines the duration in milliseconds for the delay between trigger of each repeat of
|
||||
* {@link #mRepetitiveKeys}. The default value is defined by {@link #DEFAULT_LONG_PRESS_REPEAT_DELAY}.
|
||||
* The duration must be in between {@link #MIN_LONG_PRESS__REPEAT_DELAY} and
|
||||
* {@link #MAX_LONG_PRESS__REPEAT_DELAY}, otherwise {@link #DEFAULT_LONG_PRESS_REPEAT_DELAY} is used.
|
||||
*/
|
||||
private int mLongPressRepeatDelay;
|
||||
|
||||
|
||||
/** The popup window shown if {@link ExtraKeyButton#getPopup()} returns a {@code non-null} value
|
||||
* and a swipe up action is done on an extra key. */
|
||||
private PopupWindow mPopupWindow;
|
||||
|
||||
private ScheduledExecutorService mScheduledExecutor;
|
||||
private Handler mHandler;
|
||||
private SpecialButtonsLongHoldRunnable mSpecialButtonsLongHoldRunnable;
|
||||
private int mLongPressCount;
|
||||
|
||||
|
||||
public ExtraKeysView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
setRepetitiveKeys(ExtraKeysConstants.PRIMARY_REPETITIVE_KEYS);
|
||||
setSpecialButtons(getDefaultSpecialButtons(this));
|
||||
setButtonColors(DEFAULT_BUTTON_TEXT_COLOR, DEFAULT_BUTTON_ACTIVE_TEXT_COLOR,
|
||||
DEFAULT_BUTTON_BACKGROUND_COLOR, DEFAULT_BUTTON_ACTIVE_BACKGROUND_COLOR);
|
||||
setLongPressTimeout(ViewConfiguration.getLongPressTimeout());
|
||||
setLongPressRepeatDelay(DEFAULT_LONG_PRESS_REPEAT_DELAY);
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mExtraKeysViewClient}. */
|
||||
public IExtraKeysView getExtraKeysViewClient() {
|
||||
return mExtraKeysViewClient;
|
||||
}
|
||||
|
||||
/** Set {@link #mExtraKeysViewClient}. */
|
||||
public void setExtraKeysViewClient(IExtraKeysView extraKeysViewClient) {
|
||||
mExtraKeysViewClient = extraKeysViewClient;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mRepetitiveKeys}. */
|
||||
public List<String> getRepetitiveKeys() {
|
||||
if (mRepetitiveKeys == null) return null;
|
||||
return mRepetitiveKeys.stream().map(String::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/** Set {@link #mRepetitiveKeys}. Must not be {@code null}. */
|
||||
public void setRepetitiveKeys(@NonNull List<String> repetitiveKeys) {
|
||||
mRepetitiveKeys = repetitiveKeys;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mSpecialButtons}. */
|
||||
public Map<SpecialButton, SpecialButtonState> getSpecialButtons() {
|
||||
if (mSpecialButtons == null) return null;
|
||||
return mSpecialButtons.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
|
||||
}
|
||||
|
||||
/** Get {@link #mSpecialButtonsKeys}. */
|
||||
public Set<String> getSpecialButtonsKeys() {
|
||||
if (mSpecialButtonsKeys == null) return null;
|
||||
return mSpecialButtonsKeys.stream().map(String::new).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/** Set {@link #mSpecialButtonsKeys}. Must not be {@code null}. */
|
||||
public void setSpecialButtons(@NonNull Map<SpecialButton, SpecialButtonState> specialButtons) {
|
||||
mSpecialButtons = specialButtons;
|
||||
mSpecialButtonsKeys = this.mSpecialButtons.keySet().stream().map(SpecialButton::getKey).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set the {@link ExtraKeysView} button colors.
|
||||
*
|
||||
* @param buttonTextColor The value for {@link #mButtonTextColor}.
|
||||
* @param buttonActiveTextColor The value for {@link #mButtonActiveTextColor}.
|
||||
* @param buttonBackgroundColor The value for {@link #mButtonBackgroundColor}.
|
||||
* @param buttonActiveBackgroundColor The value for {@link #mButtonActiveBackgroundColor}.
|
||||
*/
|
||||
public void setButtonColors(int buttonTextColor, int buttonActiveTextColor, int buttonBackgroundColor, int buttonActiveBackgroundColor) {
|
||||
mButtonTextColor = buttonTextColor;
|
||||
mButtonActiveTextColor = buttonActiveTextColor;
|
||||
mButtonBackgroundColor = buttonBackgroundColor;
|
||||
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonTextColor}. */
|
||||
public int getButtonTextColor() {
|
||||
return mButtonTextColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonTextColor}. */
|
||||
public void setButtonTextColor(int buttonTextColor) {
|
||||
mButtonTextColor = buttonTextColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonActiveTextColor}. */
|
||||
public int getButtonActiveTextColor() {
|
||||
return mButtonActiveTextColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonActiveTextColor}. */
|
||||
public void setButtonActiveTextColor(int buttonActiveTextColor) {
|
||||
mButtonActiveTextColor = buttonActiveTextColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonBackgroundColor}. */
|
||||
public int getButtonBackgroundColor() {
|
||||
return mButtonBackgroundColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonBackgroundColor}. */
|
||||
public void setButtonBackgroundColor(int buttonBackgroundColor) {
|
||||
mButtonBackgroundColor = buttonBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mButtonActiveBackgroundColor}. */
|
||||
public int getButtonActiveBackgroundColor() {
|
||||
return mButtonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonActiveBackgroundColor}. */
|
||||
public void setButtonActiveBackgroundColor(int buttonActiveBackgroundColor) {
|
||||
mButtonActiveBackgroundColor = buttonActiveBackgroundColor;
|
||||
}
|
||||
|
||||
/** Set {@link #mButtonTextAllCaps}. */
|
||||
public void setButtonTextAllCaps(boolean buttonTextAllCaps) {
|
||||
mButtonTextAllCaps = buttonTextAllCaps;
|
||||
}
|
||||
|
||||
|
||||
/** Get {@link #mLongPressTimeout}. */
|
||||
public int getLongPressTimeout() {
|
||||
return mLongPressTimeout;
|
||||
}
|
||||
|
||||
/** Set {@link #mLongPressTimeout}. */
|
||||
public void setLongPressTimeout(int longPressDuration) {
|
||||
if (longPressDuration >= MIN_LONG_PRESS_DURATION && longPressDuration <= MAX_LONG_PRESS_DURATION) {
|
||||
mLongPressTimeout = longPressDuration;
|
||||
} else {
|
||||
mLongPressTimeout = FALLBACK_LONG_PRESS_DURATION;
|
||||
}
|
||||
}
|
||||
|
||||
/** Get {@link #mLongPressRepeatDelay}. */
|
||||
public int getLongPressRepeatDelay() {
|
||||
return mLongPressRepeatDelay;
|
||||
}
|
||||
|
||||
/** Set {@link #mLongPressRepeatDelay}. */
|
||||
public void setLongPressRepeatDelay(int longPressRepeatDelay) {
|
||||
if (mLongPressRepeatDelay >= MIN_LONG_PRESS__REPEAT_DELAY && mLongPressRepeatDelay <= MAX_LONG_PRESS__REPEAT_DELAY) {
|
||||
mLongPressRepeatDelay = longPressRepeatDelay;
|
||||
} else {
|
||||
mLongPressRepeatDelay = DEFAULT_LONG_PRESS_REPEAT_DELAY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Get the default map that can be used for {@link #mSpecialButtons}. */
|
||||
@NonNull
|
||||
public Map<SpecialButton, SpecialButtonState> getDefaultSpecialButtons(ExtraKeysView extraKeysView) {
|
||||
return new HashMap<SpecialButton, SpecialButtonState>() {{
|
||||
put(SpecialButton.CTRL, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.ALT, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.SHIFT, new SpecialButtonState(extraKeysView));
|
||||
put(SpecialButton.FN, new SpecialButtonState(extraKeysView));
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Reload this instance of {@link ExtraKeysView} with the info passed in {@code extraKeysInfo}.
|
||||
*
|
||||
* @param extraKeysInfo The {@link ExtraKeysInfo} that defines the necessary info for the extra keys.
|
||||
*/
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
public void reload(ExtraKeysInfo extraKeysInfo) {
|
||||
if (extraKeysInfo == null)
|
||||
return;
|
||||
|
||||
for(SpecialButtonState state : mSpecialButtons.values())
|
||||
state.buttons = new ArrayList<>();
|
||||
|
||||
removeAllViews();
|
||||
|
||||
ExtraKeyButton[][] buttons = extraKeysInfo.getMatrix();
|
||||
|
||||
setRowCount(buttons.length);
|
||||
setColumnCount(maximumLength(buttons));
|
||||
|
||||
for (int row = 0; row < buttons.length; row++) {
|
||||
for (int col = 0; col < buttons[row].length; col++) {
|
||||
final ExtraKeyButton buttonInfo = buttons[row][col];
|
||||
|
||||
Button button;
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
button = createSpecialButton(buttonInfo.getKey(), true);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
}
|
||||
|
||||
button.setText(buttonInfo.getDisplay());
|
||||
button.setTextColor(mButtonTextColor);
|
||||
button.setAllCaps(mButtonTextAllCaps);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
|
||||
button.setOnClickListener(view -> {
|
||||
performExtraKeyButtonHapticFeedback(view, buttonInfo, button);
|
||||
onAnyExtraKeyButtonClick(view, buttonInfo, button);
|
||||
});
|
||||
|
||||
button.setOnTouchListener((view, event) -> {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
view.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
// Start long press scheduled executors which will be stopped in next MotionEvent
|
||||
startScheduledExecutors(view, buttonInfo, button);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
// Show popup on swipe up
|
||||
if (mPopupWindow == null && event.getY() < 0) {
|
||||
stopScheduledExecutors();
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
showPopup(view, buttonInfo.getPopup());
|
||||
}
|
||||
if (mPopupWindow != null && event.getY() > 0) {
|
||||
view.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
dismissPopup();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
stopScheduledExecutors();
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_UP:
|
||||
view.setBackgroundColor(mButtonBackgroundColor);
|
||||
stopScheduledExecutors();
|
||||
// If ACTION_UP up was not from a repetitive key or was with a key with a popup button
|
||||
if (mLongPressCount == 0 || mPopupWindow != null) {
|
||||
// Trigger popup button click if swipe up complete
|
||||
if (mPopupWindow != null) {
|
||||
dismissPopup();
|
||||
if (buttonInfo.getPopup() != null) {
|
||||
onAnyExtraKeyButtonClick(view, buttonInfo.getPopup(), button);
|
||||
}
|
||||
} else {
|
||||
view.performClick();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
LayoutParams param = new GridLayout.LayoutParams();
|
||||
param.width = 0;
|
||||
param.height = 0;
|
||||
param.setMargins(0, 0, 0, 0);
|
||||
param.columnSpec = GridLayout.spec(col, GridLayout.FILL, 1.f);
|
||||
param.rowSpec = GridLayout.spec(row, GridLayout.FILL, 1.f);
|
||||
button.setLayoutParams(param);
|
||||
|
||||
addView(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void onExtraKeyButtonClick(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (mExtraKeysViewClient != null)
|
||||
mExtraKeysViewClient.onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}
|
||||
|
||||
private void performExtraKeyButtonHapticFeedback(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
if (mExtraKeysViewClient != null) {
|
||||
// If client handled the feedback, then just return
|
||||
if (mExtraKeysViewClient.performExtraKeyButtonHapticFeedback(view, buttonInfo, button))
|
||||
return;
|
||||
}
|
||||
|
||||
if (Settings.System.getInt(getContext().getContentResolver(),
|
||||
Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) != 0) {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 28) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
} else {
|
||||
// Perform haptic feedback only if no total silence mode enabled.
|
||||
if (Settings.Global.getInt(getContext().getContentResolver(), "zen_mode", 0) != 2) {
|
||||
button.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void onAnyExtraKeyButtonClick(View view, @NonNull ExtraKeyButton buttonInfo, Button button) {
|
||||
if (isSpecialButton(buttonInfo)) {
|
||||
if (mLongPressCount > 0) return;
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
|
||||
// Toggle active state and disable lock state if new state is not active
|
||||
state.setIsActive(!state.isActive);
|
||||
if (!state.isActive)
|
||||
state.setIsLocked(false);
|
||||
} else {
|
||||
onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void startScheduledExecutors(View view, ExtraKeyButton buttonInfo, Button button) {
|
||||
stopScheduledExecutors();
|
||||
mLongPressCount = 0;
|
||||
if (mRepetitiveKeys.contains(buttonInfo.getKey())) {
|
||||
// Auto repeat key if long pressed until ACTION_UP stops it by calling stopScheduledExecutors.
|
||||
// Currently, only one (last) repeat key can run at a time. Old ones are stopped.
|
||||
mScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
|
||||
mScheduledExecutor.scheduleWithFixedDelay(() -> {
|
||||
mLongPressCount++;
|
||||
onExtraKeyButtonClick(view, buttonInfo, button);
|
||||
}, mLongPressTimeout, mLongPressRepeatDelay, TimeUnit.MILLISECONDS);
|
||||
} else if (isSpecialButton(buttonInfo)) {
|
||||
// Lock the key if long pressed by running mSpecialButtonsLongHoldRunnable after
|
||||
// waiting for mLongPressTimeout milliseconds. If user does not long press, then the
|
||||
// ACTION_UP triggered will cancel the runnable by calling stopScheduledExecutors before
|
||||
// it has a chance to run.
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonInfo.getKey()));
|
||||
if (state == null) return;
|
||||
if (mHandler == null)
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mSpecialButtonsLongHoldRunnable = new SpecialButtonsLongHoldRunnable(state);
|
||||
mHandler.postDelayed(mSpecialButtonsLongHoldRunnable, mLongPressTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopScheduledExecutors() {
|
||||
if (mScheduledExecutor != null) {
|
||||
mScheduledExecutor.shutdownNow();
|
||||
mScheduledExecutor = null;
|
||||
}
|
||||
|
||||
if (mSpecialButtonsLongHoldRunnable != null && mHandler != null) {
|
||||
mHandler.removeCallbacks(mSpecialButtonsLongHoldRunnable);
|
||||
mSpecialButtonsLongHoldRunnable = null;
|
||||
}
|
||||
}
|
||||
|
||||
private class SpecialButtonsLongHoldRunnable implements Runnable {
|
||||
private final SpecialButtonState mState;
|
||||
|
||||
public SpecialButtonsLongHoldRunnable(SpecialButtonState state) {
|
||||
mState = state;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
// Toggle active and lock state
|
||||
mState.setIsLocked(!mState.isActive);
|
||||
mState.setIsActive(!mState.isActive);
|
||||
mLongPressCount++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
void showPopup(View view, ExtraKeyButton extraButton) {
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
Button button;
|
||||
if (isSpecialButton(extraButton)) {
|
||||
button = createSpecialButton(extraButton.getKey(), false);
|
||||
if (button == null) return;
|
||||
} else {
|
||||
button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(mButtonTextColor);
|
||||
}
|
||||
button.setText(extraButton.getDisplay());
|
||||
button.setAllCaps(mButtonTextAllCaps);
|
||||
button.setPadding(0, 0, 0, 0);
|
||||
button.setMinHeight(0);
|
||||
button.setMinWidth(0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinimumHeight(0);
|
||||
button.setWidth(width);
|
||||
button.setHeight(height);
|
||||
button.setBackgroundColor(mButtonActiveBackgroundColor);
|
||||
mPopupWindow = new PopupWindow(this);
|
||||
mPopupWindow.setWidth(LayoutParams.WRAP_CONTENT);
|
||||
mPopupWindow.setHeight(LayoutParams.WRAP_CONTENT);
|
||||
mPopupWindow.setContentView(button);
|
||||
mPopupWindow.setOutsideTouchable(true);
|
||||
mPopupWindow.setFocusable(false);
|
||||
mPopupWindow.showAsDropDown(view, 0, -2 * height);
|
||||
}
|
||||
|
||||
private void dismissPopup() {
|
||||
mPopupWindow.setContentView(null);
|
||||
mPopupWindow.dismiss();
|
||||
mPopupWindow = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Check whether a {@link ExtraKeyButton} is a {@link SpecialButton}. */
|
||||
public boolean isSpecialButton(ExtraKeyButton button) {
|
||||
return mSpecialButtonsKeys.contains(button.getKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Read whether {@link SpecialButton} registered in {@link #mSpecialButtons} is active or not.
|
||||
*
|
||||
* @param specialButton The {@link SpecialButton} to read.
|
||||
* @param autoSetInActive Set to {@code true} if {@link SpecialButtonState#isActive} should be
|
||||
* set {@code false} if button is not locked.
|
||||
* @return Returns {@code null} if button does not exist in {@link #mSpecialButtons}. If button
|
||||
* exists, then returns {@code true} if the button is created in {@link ExtraKeysView}
|
||||
* and is active, otherwise {@code false}.
|
||||
*/
|
||||
@Nullable
|
||||
public Boolean readSpecialButton(SpecialButton specialButton, boolean autoSetInActive) {
|
||||
SpecialButtonState state = mSpecialButtons.get(specialButton);
|
||||
if (state == null) return null;
|
||||
|
||||
if (!state.isCreated || !state.isActive)
|
||||
return false;
|
||||
|
||||
// Disable active state only if not locked
|
||||
if (autoSetInActive && !state.isLocked)
|
||||
state.setIsActive(false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Button createSpecialButton(String buttonKey, boolean needUpdate) {
|
||||
SpecialButtonState state = mSpecialButtons.get(SpecialButton.valueOf(buttonKey));
|
||||
if (state == null) return null;
|
||||
state.setIsCreated(true);
|
||||
Button button = new Button(getContext(), null, android.R.attr.buttonBarButtonStyle);
|
||||
button.setTextColor(state.isActive ? mButtonActiveTextColor : mButtonTextColor);
|
||||
if (needUpdate) {
|
||||
state.buttons.add(button);
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* General util function to compute the longest column length in a matrix.
|
||||
*/
|
||||
static int maximumLength(Object[][] matrix) {
|
||||
int m = 0;
|
||||
for (Object[] row : matrix)
|
||||
m = Math.max(m, row.length);
|
||||
return m;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/** The {@link Class} that implements special buttons for {@link ExtraKeysView}. */
|
||||
public class SpecialButton {
|
||||
|
||||
private static final HashMap<String, SpecialButton> map = new HashMap<>();
|
||||
|
||||
public static final SpecialButton CTRL = new SpecialButton("CTRL");
|
||||
public static final SpecialButton ALT = new SpecialButton("ALT");
|
||||
public static final SpecialButton SHIFT = new SpecialButton("SHIFT");
|
||||
public static final SpecialButton FN = new SpecialButton("FN");
|
||||
|
||||
/** The special button key. */
|
||||
private final String key;
|
||||
|
||||
/**
|
||||
* Initialize a {@link SpecialButton}.
|
||||
*
|
||||
* @param key The unique key name for the special button. The key is registered in {@link #map}
|
||||
* with which the {@link SpecialButton} can be retrieved via a call to
|
||||
* {@link #valueOf(String)}.
|
||||
*/
|
||||
public SpecialButton(@NonNull final String key) {
|
||||
this.key = key;
|
||||
map.put(key, this);
|
||||
}
|
||||
|
||||
/** Get {@link #key} for this {@link SpecialButton}. */
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link SpecialButton} for {@code key}.
|
||||
*
|
||||
* @param key The unique key name for the special button.
|
||||
*/
|
||||
public static SpecialButton valueOf(String key) {
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return key;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.termux.shared.terminal.io.extrakeys;
|
||||
|
||||
import android.widget.Button;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** The {@link Class} that maintains a state of a {@link SpecialButton} */
|
||||
public class SpecialButtonState {
|
||||
|
||||
/** If special button has been created for the {@link ExtraKeysView}. */
|
||||
boolean isCreated = false;
|
||||
/** If special button is active. */
|
||||
boolean isActive = false;
|
||||
/** If special button is locked due to long hold on it and should not be deactivated if its
|
||||
* state is read. */
|
||||
boolean isLocked = false;
|
||||
|
||||
List<Button> buttons = new ArrayList<>();
|
||||
|
||||
ExtraKeysView mExtraKeysView;
|
||||
|
||||
/**
|
||||
* Initialize a {@link SpecialButtonState} to maintain state of a {@link SpecialButton}.
|
||||
*
|
||||
* @param extraKeysView The {@link ExtraKeysView} instance in which the {@link SpecialButton}
|
||||
* is to be registered.
|
||||
*/
|
||||
public SpecialButtonState(ExtraKeysView extraKeysView) {
|
||||
mExtraKeysView = extraKeysView;
|
||||
}
|
||||
|
||||
/** Set {@link #isCreated}. */
|
||||
public void setIsCreated(boolean value) {
|
||||
isCreated = value;
|
||||
}
|
||||
|
||||
/** Set {@link #isActive}. */
|
||||
public void setIsActive(boolean value) {
|
||||
isActive = value;
|
||||
buttons.forEach(button -> button.setTextColor(value ? mExtraKeysView.getButtonActiveTextColor() : mExtraKeysView.getButtonTextColor()));
|
||||
}
|
||||
|
||||
/** Set {@link #isLocked}. */
|
||||
public void setIsLocked(boolean value) {
|
||||
isLocked = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
package com.termux.shared.termux;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.os.Build;
|
||||
import android.system.Os;
|
||||
import android.system.OsConstants;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.termux.shared.android.SELinuxUtils;
|
||||
import com.termux.shared.data.DataUtils;
|
||||
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.Properties;
|
||||
import java.util.TimeZone;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class AndroidUtils {
|
||||
|
||||
/**
|
||||
* 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 getAppInfoMarkdownString(@NonNull final Context context) {
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
ApplicationInfo applicationInfo = context.getApplicationInfo();
|
||||
if (applicationInfo == null) return null;
|
||||
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"APP_NAME", PackageUtils.getAppNameForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"PACKAGE_NAME", PackageUtils.getPackageNameForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"VERSION_NAME", PackageUtils.getVersionNameForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"VERSION_CODE", PackageUtils.getVersionCodeForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"TARGET_SDK", PackageUtils.getTargetSDKForPackage(context));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_DEBUGGABLE_BUILD", PackageUtils.isAppForPackageADebuggableBuild(context));
|
||||
|
||||
if (PackageUtils.isAppInstalledOnExternalStorage(context)) {
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"IS_INSTALLED_ON_EXTERNAL_STORAGE", true);
|
||||
}
|
||||
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_PROCESS_CONTEXT", SELinuxUtils.getContext());
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_FILE_CONTEXT", SELinuxUtils.getFileContext(context.getFilesDir().getAbsolutePath()));
|
||||
|
||||
String seInfoUser = PackageUtils.getApplicationInfoSeInfoUserForPackage(applicationInfo);
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SE_INFO", PackageUtils.getApplicationInfoSeInfoForPackage(applicationInfo) +
|
||||
(DataUtils.isNullOrEmpty(seInfoUser) ? "" : seInfoUser));
|
||||
|
||||
String filesDir = context.getFilesDir().getAbsolutePath();
|
||||
if (!filesDir.equals("/data/user/0/" + context.getPackageName() + "/files") &&
|
||||
!filesDir.equals("/data/data/" + context.getPackageName() + "/files"))
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"FILES_DIR", filesDir);
|
||||
|
||||
Long userId = PackageUtils.getSerialNumberForCurrentUser(context);
|
||||
if (userId == null || userId != 0)
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"USER_ID", userId);
|
||||
|
||||
AndroidUtils.appendPropertyToMarkdownIfSet(markdownString,"PROFILE_OWNER", PackageUtils.getProfileOwnerPackageNameForUser(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) {
|
||||
// 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, "ID", Build.ID);
|
||||
appendPropertyToMarkdown(markdownString, "DISPLAY", Build.DISPLAY);
|
||||
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, "BOARD", Build.BOARD);
|
||||
appendPropertyToMarkdown(markdownString, "HARDWARE", Build.HARDWARE);
|
||||
appendPropertyToMarkdown(markdownString, "DEVICE", Build.DEVICE);
|
||||
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_ABIS));
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_32_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_32_BIT_ABIS));
|
||||
appendPropertyToMarkdown(markdownString, "SUPPORTED_64_BIT_ABIS", Joiner.on(", ").skipNulls().join(Build.SUPPORTED_64_BIT_ABIS));
|
||||
|
||||
// If on Android >= 15
|
||||
if (Build.VERSION.SDK_INT >= 35) {
|
||||
try {
|
||||
appendPropertyToMarkdownIfSet(markdownString, "PAGE_SIZE", Os.sysconf(OsConstants._SC_PAGESIZE));
|
||||
} catch (Throwable t) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public 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;
|
||||
}
|
||||
}
|
||||
|
||||
public 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));
|
||||
}
|
||||
|
||||
public static void appendPropertyToMarkdown(StringBuilder markdownString, String label, Object value) {
|
||||
markdownString.append("\n").append(getPropertyMarkdown(label, value));
|
||||
}
|
||||
|
||||
public 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());
|
||||
}
|
||||
|
||||
public static String getCurrentMilliSecondUTCTimeStamp() {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS z");
|
||||
df.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
public static String getCurrentMilliSecondLocalTimeStamp() {
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss.SSS");
|
||||
df.setTimeZone(TimeZone.getDefault());
|
||||
return df.format(new Date());
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,545 @@
|
|||
package com.termux.shared.termux;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ResolveInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.termux.shared.R;
|
||||
import com.termux.shared.file.FileUtils;
|
||||
import com.termux.shared.file.TermuxFileUtils;
|
||||
import com.termux.shared.logger.Logger;
|
||||
import com.termux.shared.markdown.MarkdownUtils;
|
||||
import com.termux.shared.models.ExecutionCommand;
|
||||
import com.termux.shared.models.errors.Error;
|
||||
import com.termux.shared.packages.PackageUtils;
|
||||
import com.termux.shared.shell.TermuxShellEnvironmentClient;
|
||||
import com.termux.shared.shell.TermuxTask;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class TermuxUtils {
|
||||
|
||||
private static final String LOG_TAG = "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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Check if Termux app is installed and enabled. This can be used by external apps that don't
|
||||
* share `sharedUserId` with the Termux app.
|
||||
*
|
||||
* If your third-party app is targeting sdk `30` (android `11`), then it needs to add `com.termux`
|
||||
* package to the `queries` element or request `QUERY_ALL_PACKAGES` permission in its
|
||||
* `AndroidManifest.xml`. Otherwise it will get `PackageSetting{...... com.termux/......} BLOCKED`
|
||||
* errors in `logcat` and `RUN_COMMAND` won't work.
|
||||
* Check [package-visibility](https://developer.android.com/training/basics/intents/package-visibility#package-name),
|
||||
* `QUERY_ALL_PACKAGES` [googleplay policy](https://support.google.com/googleplay/android-developer/answer/10158779
|
||||
* and this [article](https://medium.com/androiddevelopers/working-with-package-visibility-dc252829de2d) for more info.
|
||||
*
|
||||
* {@code
|
||||
* <manifest
|
||||
* <queries>
|
||||
* <package android:name="com.termux" />
|
||||
* </queries>
|
||||
* </manifest>
|
||||
* }
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns {@code errmsg} if {@link TermuxConstants#TERMUX_PACKAGE_NAME} is not installed
|
||||
* or disabled, otherwise {@code null}.
|
||||
*/
|
||||
public static String isTermuxAppInstalled(@NonNull final Context context) {
|
||||
return PackageUtils.isAppInstalled(context, TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Termux:API app is installed and enabled. This can be used by external apps that don't
|
||||
* share `sharedUserId` with the Termux:API app.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns {@code errmsg} if {@link TermuxConstants#TERMUX_API_PACKAGE_NAME} is not installed
|
||||
* or disabled, otherwise {@code null}.
|
||||
*/
|
||||
public static String isTermuxAPIAppInstalled(@NonNull final Context context) {
|
||||
return PackageUtils.isAppInstalled(context, TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Termux app is installed and accessible. This can only be used by apps that share
|
||||
* `sharedUserId` with the Termux app.
|
||||
*
|
||||
* This is done by checking if first checking if app is installed and enabled and then if
|
||||
* {@code currentPackageContext} can be used to get the {@link Context} of the app with
|
||||
* {@link TermuxConstants#TERMUX_PACKAGE_NAME} and then if
|
||||
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} exists and has
|
||||
* {@link FileUtils#APP_WORKING_DIRECTORY_PERMISSIONS} permissions. The directory will not
|
||||
* be automatically created and neither the missing permissions automatically set.
|
||||
*
|
||||
* @param currentPackageContext The context of current package.
|
||||
* @return Returns {@code errmsg} if failed to get termux package {@link Context} or
|
||||
* {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} is accessible, otherwise {@code null}.
|
||||
*/
|
||||
public static String isTermuxAppAccessible(@NonNull final Context currentPackageContext) {
|
||||
String errmsg = isTermuxAppInstalled(currentPackageContext);
|
||||
if (errmsg == null) {
|
||||
Context termuxPackageContext = TermuxUtils.getTermuxPackageContext(currentPackageContext);
|
||||
// If failed to get Termux app package context
|
||||
if (termuxPackageContext == null)
|
||||
errmsg = currentPackageContext.getString(R.string.error_termux_app_package_context_not_accessible);
|
||||
|
||||
if (errmsg == null) {
|
||||
// If TermuxConstants.TERMUX_PREFIX_DIR_PATH is not a directory or does not have required permissions
|
||||
Error error = TermuxFileUtils.isTermuxPrefixDirectoryAccessible(false, false);
|
||||
if (error != null)
|
||||
errmsg = currentPackageContext.getString(R.string.error_termux_prefix_dir_path_not_accessible,
|
||||
PackageUtils.getAppNameForPackage(currentPackageContext));
|
||||
}
|
||||
}
|
||||
|
||||
if (errmsg != null)
|
||||
return errmsg + " " + currentPackageContext.getString(R.string.msg_termux_app_required_by_app,
|
||||
PackageUtils.getAppNameForPackage(currentPackageContext));
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
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 apps info of all/any termux plugin apps installed.
|
||||
*
|
||||
* @param currentPackageContext The context of current package.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getTermuxPluginAppsInfoMarkdownString(final Context currentPackageContext) {
|
||||
if (currentPackageContext == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
List<String> termuxPluginAppPackageNamesList = TermuxConstants.TERMUX_PLUGIN_APP_PACKAGE_NAMES_LIST;
|
||||
|
||||
if (termuxPluginAppPackageNamesList != null) {
|
||||
for (int i = 0; i < termuxPluginAppPackageNamesList.size(); i++) {
|
||||
String termuxPluginAppPackageName = termuxPluginAppPackageNamesList.get(i);
|
||||
Context termuxPluginAppContext = PackageUtils.getContextForPackage(currentPackageContext, termuxPluginAppPackageName);
|
||||
// If the package context for the plugin app is not null, then assume its installed and get its info
|
||||
if (termuxPluginAppContext != null) {
|
||||
if (i != 0)
|
||||
markdownString.append("\n\n");
|
||||
markdownString.append(getAppInfoMarkdownString(termuxPluginAppContext, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (markdownString.toString().isEmpty())
|
||||
return null;
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(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 && termuxPackageContext != null && !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();
|
||||
|
||||
markdownString.append((AndroidUtils.getAppInfoMarkdownString(context)));
|
||||
|
||||
Error error;
|
||||
error = TermuxFileUtils.isTermuxFilesDirectoryAccessible(context, true, true);
|
||||
if (error != null) {
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString, "TERMUX_FILES_DIR", TermuxConstants.TERMUX_FILES_DIR_PATH);
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString, "IS_TERMUX_FILES_DIR_ACCESSIBLE", "false - " + Error.getMinimalErrorString(error));
|
||||
}
|
||||
|
||||
String signingCertificateSHA256Digest = PackageUtils.getSigningCertificateSHA256DigestForPackage(context);
|
||||
if (signingCertificateSHA256Digest != null) {
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"APK_RELEASE", getAPKRelease(signingCertificateSHA256Digest));
|
||||
AndroidUtils.appendPropertyToMarkdown(markdownString,"SIGNING_CERTIFICATE_SHA256_DIGEST", signingCertificateSHA256Digest);
|
||||
}
|
||||
|
||||
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, TermuxConstants.TERMUX_WIKI_URL)).append("\n");
|
||||
|
||||
markdownString.append("\n\n### Email\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_SUPPORT_EMAIL_URL, 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for important links.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getImportantLinksMarkdownString(@NonNull final Context context) {
|
||||
if (context == null) return "null";
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
markdownString.append("## Important Links");
|
||||
|
||||
markdownString.append("\n\n### Github\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_API_APP_NAME, TermuxConstants.TERMUX_API_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_BOOT_APP_NAME, TermuxConstants.TERMUX_BOOT_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_FLOAT_APP_NAME, TermuxConstants.TERMUX_FLOAT_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_STYLING_APP_NAME, TermuxConstants.TERMUX_STYLING_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_TASKER_APP_NAME, TermuxConstants.TERMUX_TASKER_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIDGET_APP_NAME, TermuxConstants.TERMUX_WIDGET_GITHUB_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_URL)).append(" ");
|
||||
|
||||
markdownString.append("\n\n### Email\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_SUPPORT_EMAIL_URL, 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### Wiki\n");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_WIKI, TermuxConstants.TERMUX_WIKI_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_APP_NAME, TermuxConstants.TERMUX_GITHUB_WIKI_REPO_URL)).append(" ");
|
||||
markdownString.append("\n").append(MarkdownUtils.getLinkMarkdownString(TermuxConstants.TERMUX_PACKAGES_GITHUB_REPO_NAME, TermuxConstants.TERMUX_PACKAGES_GITHUB_WIKI_REPO_URL)).append(" ");
|
||||
|
||||
markdownString.append("\n##\n");
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for APT info of the app.
|
||||
*
|
||||
* This will take a few seconds to run due to running {@code apt update} command.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String geAPTInfoMarkdownString(@NonNull final Context context) {
|
||||
|
||||
String aptInfoScript;
|
||||
InputStream inputStream = context.getResources().openRawResource(com.termux.shared.R.raw.apt_info_script);
|
||||
try {
|
||||
aptInfoScript = IOUtils.toString(inputStream, Charset.defaultCharset());
|
||||
} catch (IOException e) {
|
||||
Logger.logError(LOG_TAG, "Failed to get APT info script: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
|
||||
if (aptInfoScript == null || aptInfoScript.isEmpty()) {
|
||||
Logger.logError(LOG_TAG, "The APT info script is null or empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
aptInfoScript = aptInfoScript.replaceAll(Pattern.quote("@TERMUX_PREFIX@"), TermuxConstants.TERMUX_PREFIX_DIR_PATH);
|
||||
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(1, TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH + "/bash", null, aptInfoScript, null, true, false);
|
||||
executionCommand.commandLabel = "APT Info Command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
|
||||
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||
if (termuxTask == null || !executionCommand.isSuccessful() || executionCommand.resultData.exitCode != 0) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!executionCommand.resultData.stderr.toString().isEmpty())
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
|
||||
markdownString.append("## ").append(TermuxConstants.TERMUX_APP_NAME).append(" APT Info\n\n");
|
||||
markdownString.append(executionCommand.resultData.stdout.toString());
|
||||
markdownString.append("\n##\n");
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for info for termux debugging.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getTermuxDebugMarkdownString(@NonNull final Context context) {
|
||||
String statInfo = TermuxFileUtils.getTermuxFilesStatMarkdownString(context);
|
||||
String logcatInfo = getLogcatDumpMarkdownString(context);
|
||||
|
||||
if (statInfo != null && logcatInfo != null)
|
||||
return statInfo + "\n\n" + logcatInfo;
|
||||
else if (statInfo != null)
|
||||
return statInfo;
|
||||
else
|
||||
return logcatInfo;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a markdown {@link String} for logcat command dump.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the markdown {@link String}.
|
||||
*/
|
||||
public static String getLogcatDumpMarkdownString(@NonNull final Context context) {
|
||||
// Build script
|
||||
// We need to prevent OutOfMemoryError since StreamGobbler StringBuilder + StringBuilder.toString()
|
||||
// may require lot of memory if dump is too large.
|
||||
// Putting a limit at 3000 lines. Assuming average 160 chars/line will result in 500KB usage
|
||||
// per object.
|
||||
// That many lines should be enough for debugging for recent issues anyways assuming termux
|
||||
// has not been granted READ_LOGS permission s.
|
||||
String logcatScript = "/system/bin/logcat -d -t 3000 2>&1";
|
||||
|
||||
// Run script
|
||||
// Logging must be disabled for output of logcat command itself in StreamGobbler
|
||||
ExecutionCommand executionCommand = new ExecutionCommand(1, "/system/bin/sh", null, logcatScript + "\n", "/", true, true);
|
||||
executionCommand.commandLabel = "Logcat dump command";
|
||||
executionCommand.backgroundCustomLogLevel = Logger.LOG_LEVEL_OFF;
|
||||
TermuxTask termuxTask = TermuxTask.execute(context, executionCommand, null, new TermuxShellEnvironmentClient(), true);
|
||||
if (termuxTask == null || !executionCommand.isSuccessful()) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
return null;
|
||||
}
|
||||
|
||||
// Build script output
|
||||
StringBuilder logcatOutput = new StringBuilder();
|
||||
logcatOutput.append("$ ").append(logcatScript);
|
||||
logcatOutput.append("\n").append(executionCommand.resultData.stdout.toString());
|
||||
|
||||
boolean stderrSet = !executionCommand.resultData.stderr.toString().isEmpty();
|
||||
if (executionCommand.resultData.exitCode != 0 || stderrSet) {
|
||||
Logger.logErrorExtended(LOG_TAG, executionCommand.toString());
|
||||
if (stderrSet)
|
||||
logcatOutput.append("\n").append(executionCommand.resultData.stderr.toString());
|
||||
logcatOutput.append("\n").append("exit code: ").append(executionCommand.resultData.exitCode.toString());
|
||||
}
|
||||
|
||||
// Build markdown output
|
||||
StringBuilder markdownString = new StringBuilder();
|
||||
markdownString.append("## Logcat Dump\n\n");
|
||||
markdownString.append("\n\n").append(MarkdownUtils.getMarkdownCodeForString(logcatOutput.toString(), true));
|
||||
markdownString.append("\n##\n");
|
||||
|
||||
return markdownString.toString();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static String getAPKRelease(String signingCertificateSHA256Digest) {
|
||||
if (signingCertificateSHA256Digest == null) return "null";
|
||||
|
||||
switch (signingCertificateSHA256Digest.toUpperCase()) {
|
||||
case TermuxConstants.APK_RELEASE_FDROID_SIGNING_CERTIFICATE_SHA256_DIGEST:
|
||||
return TermuxConstants.APK_RELEASE_FDROID;
|
||||
case TermuxConstants.APK_RELEASE_GITHUB_SIGNING_CERTIFICATE_SHA256_DIGEST:
|
||||
return TermuxConstants.APK_RELEASE_GITHUB;
|
||||
case TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE_SIGNING_CERTIFICATE_SHA256_DIGEST:
|
||||
return TermuxConstants.APK_RELEASE_GOOGLE_PLAYSTORE;
|
||||
default:
|
||||
return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a process id of the main app process of the {@link TermuxConstants#TERMUX_PACKAGE_NAME}
|
||||
* package.
|
||||
*
|
||||
* @param context The context for operations.
|
||||
* @return Returns the process if found and running, otherwise {@code null}.
|
||||
*/
|
||||
public static String getTermuxAppPID(final Context context) {
|
||||
return PackageUtils.getPackagePID(context, TermuxConstants.TERMUX_PACKAGE_NAME);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,195 @@
|
|||
package com.termux.shared.view;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.inputmethodservice.InputMethodService;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class KeyboardUtils {
|
||||
|
||||
private static final String LOG_TAG = "KeyboardUtils";
|
||||
|
||||
public static void setSoftKeyboardVisibility(@NonNull final Runnable showSoftKeyboardRunnable, final Activity activity, final View view, final boolean visible) {
|
||||
if (visible) {
|
||||
// A Runnable with a delay is used, otherwise soft keyboard may not automatically open
|
||||
// on some devices, but still may fail
|
||||
view.postDelayed(showSoftKeyboardRunnable, 500);
|
||||
} else {
|
||||
view.removeCallbacks(showSoftKeyboardRunnable);
|
||||
hideSoftKeyboard(activity, view);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the soft keyboard. The {@link InputMethodManager#SHOW_FORCED} is passed as
|
||||
* {@code showFlags} so that keyboard is forcefully shown if it needs to be enabled.
|
||||
*
|
||||
* This is also important for soft keyboard to be shown when a hardware keyboard is connected, and
|
||||
* user has disabled the {@code Show on-screen keyboard while hardware keyboard is connected} toggle
|
||||
* in Android "Language and Input" settings but the current soft keyboard app overrides the
|
||||
* default implementation of {@link InputMethodService#onEvaluateInputViewShown()} and returns
|
||||
* {@code true}.
|
||||
*/
|
||||
public static void toggleSoftKeyboard(final Context context) {
|
||||
if (context == null) return;
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null)
|
||||
inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the soft keyboard. The {@code 0} value is passed as {@code flags} so that keyboard is
|
||||
* forcefully shown.
|
||||
*
|
||||
* This is also important for soft keyboard to be shown on app startup when a hardware keyboard
|
||||
* is connected, and user has disabled the {@code Show on-screen keyboard while hardware keyboard
|
||||
* is connected} toggle in Android "Language and Input" settings but the current soft keyboard app
|
||||
* overrides the default implementation of {@link InputMethodService#onEvaluateInputViewShown()}
|
||||
* and returns {@code true}.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/inputmethodservice/InputMethodService.java;l=1751
|
||||
*
|
||||
* Also check {@link InputMethodService#onShowInputRequested(int, boolean)} which must return
|
||||
* {@code true}, which can be done by failing its {@code ((flags&InputMethod.SHOW_EXPLICIT) == 0)}
|
||||
* check by passing {@code 0} as {@code flags}.
|
||||
* https://cs.android.com/android/platform/superproject/+/android-11.0.0_r3:frameworks/base/core/java/android/inputmethodservice/InputMethodService.java;l=2022
|
||||
*/
|
||||
public static void showSoftKeyboard(final Context context, final View view) {
|
||||
if (context == null || view == null) return;
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null)
|
||||
inputMethodManager.showSoftInput(view, 0);
|
||||
}
|
||||
|
||||
public static void hideSoftKeyboard(final Context context, final View view) {
|
||||
if (context == null || view == null) return;
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
if (inputMethodManager != null)
|
||||
inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
public static void disableSoftKeyboard(final Activity activity, final View view) {
|
||||
if (activity == null || view == null) return;
|
||||
hideSoftKeyboard(activity, view);
|
||||
setDisableSoftKeyboardFlags(activity);
|
||||
}
|
||||
|
||||
public static void setDisableSoftKeyboardFlags(final Activity activity) {
|
||||
if (activity != null && activity.getWindow() != null)
|
||||
activity.getWindow().setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM, WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
|
||||
}
|
||||
|
||||
public static void clearDisableSoftKeyboardFlags(final Activity activity) {
|
||||
if (activity != null && activity.getWindow() != null)
|
||||
activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
|
||||
}
|
||||
|
||||
public static boolean areDisableSoftKeyboardFlagsSet(final Activity activity) {
|
||||
if (activity == null || activity.getWindow() == null) return false;
|
||||
return (activity.getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM) != 0;
|
||||
}
|
||||
|
||||
public static void setSoftKeyboardAlwaysHiddenFlags(final Activity activity) {
|
||||
if (activity != null && activity.getWindow() != null)
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
|
||||
}
|
||||
|
||||
public static void setSoftInputModeAdjustResize(final Activity activity) {
|
||||
// TODO: The flag is deprecated for API 30 and WindowInset API should be used
|
||||
// https://developer.android.com/reference/android/view/WindowManager.LayoutParams#SOFT_INPUT_ADJUST_RESIZE
|
||||
// https://medium.com/androiddevelopers/animating-your-keyboard-fb776a8fb66d
|
||||
// https://stackoverflow.com/a/65194077/14686958
|
||||
if (activity != null && activity.getWindow() != null)
|
||||
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if soft keyboard is visible.
|
||||
* Does not work on android 7 but does on android 11 avd.
|
||||
*
|
||||
* @param activity The Activity of the root view for which the visibility should be checked.
|
||||
* @return Returns {@code true} if soft keyboard is visible, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isSoftKeyboardVisible(final Activity activity) {
|
||||
if (activity != null && activity.getWindow() != null) {
|
||||
WindowInsets insets = activity.getWindow().getDecorView().getRootWindowInsets();
|
||||
if (insets != null) {
|
||||
WindowInsetsCompat insetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets);
|
||||
if (insetsCompat.isVisible(WindowInsetsCompat.Type.ime())) {
|
||||
Logger.logVerbose(LOG_TAG, "Soft keyboard visible");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Logger.logVerbose(LOG_TAG, "Soft keyboard not visible");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hardware keyboard is connected.
|
||||
* Based on default implementation of {@link InputMethodService#onEvaluateInputViewShown()}.
|
||||
*
|
||||
* https://developer.android.com/guide/topics/resources/providing-resources#ImeQualifier
|
||||
*
|
||||
* @param context The Context for operations.
|
||||
* @return Returns {@code true} if device has hardware keys for text input or an external hardware
|
||||
* keyboard is connected, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean isHardKeyboardConnected(final Context context) {
|
||||
if (context == null) return false;
|
||||
|
||||
Configuration config = context.getResources().getConfiguration();
|
||||
return config.keyboard != Configuration.KEYBOARD_NOKEYS
|
||||
|| config.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if soft keyboard should be disabled based on user configuration.
|
||||
*
|
||||
* @param context The Context for operations.
|
||||
* @return Returns {@code true} if device has soft keyboard should be disabled, otherwise {@code false}.
|
||||
*/
|
||||
public static boolean shouldSoftKeyboardBeDisabled(final Context context, final boolean isSoftKeyboardEnabled, final boolean isSoftKeyboardEnabledOnlyIfNoHardware) {
|
||||
// If soft keyboard is disabled by user regardless of hardware keyboard
|
||||
if (!isSoftKeyboardEnabled) {
|
||||
return true;
|
||||
} else {
|
||||
/*
|
||||
* Currently, for this case, soft keyboard will be disabled on Termux app startup and
|
||||
* when switching back from another app. Soft keyboard can be temporarily enabled in
|
||||
* show/hide soft keyboard toggle behaviour with keyboard toggle buttons and will continue
|
||||
* to work when tapping on terminal view for opening and back button for closing, until
|
||||
* Termux app is switched to another app. After returning back, keyboard will be disabled
|
||||
* until toggle is pressed again.
|
||||
* This may also be helpful for the Lineage OS bug where if "Show soft keyboard" toggle
|
||||
* in "Language and Input" is disabled and Termux is started without a hardware keyboard
|
||||
* in landscape mode, and then the keyboard is connected and phone is rotated to portrait
|
||||
* mode and then keyboard is toggled with Termux keyboard toggle buttons, then a blank
|
||||
* space is shown in-place of the soft keyboard. Its likely related to
|
||||
* WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE which pushes up the view when
|
||||
* keyboard is opened instead of the keyboard opening on top of the view (hiding stuff).
|
||||
* If the "Show soft keyboard" toggle was disabled, then this resizing shouldn't happen.
|
||||
* But it seems resizing does happen, but keyboard is never opened since its not supposed to.
|
||||
* https://github.com/termux/termux-app/issues/1995#issuecomment-837080079
|
||||
*/
|
||||
// If soft keyboard is disabled by user only if hardware keyboard is connected
|
||||
if(isSoftKeyboardEnabledOnlyIfNoHardware) {
|
||||
boolean isHardKeyboardConnected = KeyboardUtils.isHardKeyboardConnected(context);
|
||||
Logger.logVerbose(LOG_TAG, "Hardware keyboard connected=" + isHardKeyboardConnected);
|
||||
return isHardKeyboardConnected;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package com.termux.shared.view;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.Rect;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.termux.shared.logger.Logger;
|
||||
|
||||
public class ViewUtils {
|
||||
|
||||
/** Log root view events. */
|
||||
public static boolean VIEW_UTILS_LOGGING_ENABLED = false;
|
||||
|
||||
private static final String LOG_TAG = "ViewUtils";
|
||||
|
||||
/**
|
||||
* Sets whether view utils logging is enabled or not.
|
||||
*
|
||||
* @param value The boolean value that defines the state.
|
||||
*/
|
||||
public static void setIsViewUtilsLoggingEnabled(boolean value) {
|
||||
VIEW_UTILS_LOGGING_ENABLED = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a {@link View} is fully visible and not hidden or partially covered by another view.
|
||||
*
|
||||
* https://stackoverflow.com/a/51078418/14686958
|
||||
*
|
||||
* @param view The {@link View} to check.
|
||||
* @param statusBarHeight The status bar height received by {@link View.OnApplyWindowInsetsListener}.
|
||||
* @return Returns {@code true} if view is fully visible.
|
||||
*/
|
||||
public static boolean isViewFullyVisible(View view, int statusBarHeight) {
|
||||
Rect[] windowAndViewRects = getWindowAndViewRects(view, statusBarHeight);
|
||||
if (windowAndViewRects == null)
|
||||
return false;
|
||||
return windowAndViewRects[0].contains(windowAndViewRects[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link Rect} of a {@link View} and the {@link Rect} of the window inside which it
|
||||
* exists.
|
||||
*
|
||||
* https://stackoverflow.com/a/51078418/14686958
|
||||
*
|
||||
* @param view The {@link View} inside the window whose {@link Rect} to get.
|
||||
* @param statusBarHeight The status bar height received by {@link View.OnApplyWindowInsetsListener}.
|
||||
* @return Returns {@link Rect[]} if view is visible where Rect[0] will contain window
|
||||
* {@link Rect} and Rect[1] will contain view {@link Rect}. This will be {@code null}
|
||||
* if view is not visible.
|
||||
*/
|
||||
@Nullable
|
||||
public static Rect[] getWindowAndViewRects(View view, int statusBarHeight) {
|
||||
if (view == null || !view.isShown())
|
||||
return null;
|
||||
|
||||
boolean view_utils_logging_enabled = VIEW_UTILS_LOGGING_ENABLED;
|
||||
|
||||
// windowRect - will hold available area where content remain visible to users
|
||||
// Takes into account screen decorations (e.g. statusbar)
|
||||
Rect windowRect = new Rect();
|
||||
view.getWindowVisibleDisplayFrame(windowRect);
|
||||
|
||||
// If there is actionbar, get his height
|
||||
int actionBarHeight = 0;
|
||||
boolean isInMultiWindowMode = false;
|
||||
Context context = view.getContext();
|
||||
if (context instanceof AppCompatActivity) {
|
||||
androidx.appcompat.app.ActionBar actionBar = ((AppCompatActivity) context).getSupportActionBar();
|
||||
if (actionBar != null) actionBarHeight = actionBar.getHeight();
|
||||
isInMultiWindowMode = ((AppCompatActivity) context).isInMultiWindowMode();
|
||||
} else if (context instanceof Activity) {
|
||||
android.app.ActionBar actionBar = ((Activity) context).getActionBar();
|
||||
if (actionBar != null) actionBarHeight = actionBar.getHeight();
|
||||
isInMultiWindowMode = ((Activity) context).isInMultiWindowMode();
|
||||
}
|
||||
|
||||
int displayOrientation = getDisplayOrientation(context);
|
||||
|
||||
// windowAvailableRect - takes into account actionbar and statusbar height
|
||||
Rect windowAvailableRect;
|
||||
windowAvailableRect = new Rect(windowRect.left, windowRect.top + actionBarHeight, windowRect.right, windowRect.bottom);
|
||||
|
||||
// viewRect - holds position of the view in window
|
||||
// (methods as getGlobalVisibleRect, getHitRect, getDrawingRect can return different result,
|
||||
// when partialy visible)
|
||||
Rect viewRect;
|
||||
final int[] viewsLocationInWindow = new int[2];
|
||||
view.getLocationInWindow(viewsLocationInWindow);
|
||||
int viewLeft = viewsLocationInWindow[0];
|
||||
int viewTop = viewsLocationInWindow[1];
|
||||
|
||||
if (view_utils_logging_enabled) {
|
||||
Logger.logVerbose(LOG_TAG, "getWindowAndViewRects:");
|
||||
Logger.logVerbose(LOG_TAG, "windowRect: " + toRectString(windowRect) + ", windowAvailableRect: " + toRectString(windowAvailableRect));
|
||||
Logger.logVerbose(LOG_TAG, "viewsLocationInWindow: " + toPointString(new Point(viewLeft, viewTop)));
|
||||
Logger.logVerbose(LOG_TAG, "activitySize: " + toPointString(getDisplaySize(context, true)) +
|
||||
", displaySize: " + toPointString(getDisplaySize(context, false)) +
|
||||
", displayOrientation=" + displayOrientation);
|
||||
}
|
||||
|
||||
if (isInMultiWindowMode) {
|
||||
if (displayOrientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
// The windowRect.top of the window at the of split screen mode should start right
|
||||
// below the status bar
|
||||
if (statusBarHeight != windowRect.top) {
|
||||
if (view_utils_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "Window top does not equal statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly bottom app in split screen mode. Adding windowRect.top " + windowRect.top + " to viewTop.");
|
||||
viewTop += windowRect.top;
|
||||
} else {
|
||||
if (view_utils_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "windowRect.top equals statusBarHeight " + statusBarHeight + " in multi-window portrait mode. Window is possibly top app in split screen mode.");
|
||||
}
|
||||
|
||||
} else if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
// If window is on the right in landscape mode of split screen, the viewLeft actually
|
||||
// starts at windowRect.left instead of 0 returned by getLocationInWindow
|
||||
viewLeft += windowRect.left;
|
||||
}
|
||||
}
|
||||
|
||||
int viewRight = viewLeft + view.getWidth();
|
||||
int viewBottom = viewTop + view.getHeight();
|
||||
viewRect = new Rect(viewLeft, viewTop, viewRight, viewBottom);
|
||||
|
||||
if (displayOrientation == Configuration.ORIENTATION_LANDSCAPE && viewRight > windowAvailableRect.right) {
|
||||
if (view_utils_logging_enabled)
|
||||
Logger.logVerbose(LOG_TAG, "viewRight " + viewRight + " is greater than windowAvailableRect.right " + windowAvailableRect.right + " in landscape mode. Setting windowAvailableRect.right to viewRight since it may not include navbar height.");
|
||||
windowAvailableRect.right = viewRight;
|
||||
}
|
||||
|
||||
return new Rect[]{windowAvailableRect, viewRect};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if {@link Rect} r2 is above r2. An empty rectangle never contains another rectangle.
|
||||
*
|
||||
* @param r1 The base rectangle.
|
||||
* @param r2 The rectangle being tested that should be above.
|
||||
* @return Returns {@code true} if r2 is above r1.
|
||||
*/
|
||||
public static boolean isRectAbove(@NonNull Rect r1, @NonNull Rect r2) {
|
||||
// check for empty first
|
||||
return r1.left < r1.right && r1.top < r1.bottom
|
||||
// now check if above
|
||||
&& r1.left <= r2.left && r1.bottom >= r2.bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device orientation.
|
||||
*
|
||||
* Related: https://stackoverflow.com/a/29392593/14686958
|
||||
*
|
||||
* @param context The {@link Context} to check with.
|
||||
* @return {@link Configuration#ORIENTATION_PORTRAIT} or {@link Configuration#ORIENTATION_LANDSCAPE}.
|
||||
*/
|
||||
public static int getDisplayOrientation(@NonNull Context context) {
|
||||
Point size = getDisplaySize(context, false);
|
||||
return (size.x < size.y) ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device display size.
|
||||
*
|
||||
* @param context The {@link Context} to check with. It must be {@link Activity} context, otherwise
|
||||
* android will throw:
|
||||
* `java.lang.IllegalArgumentException: Used non-visual Context to obtain an instance of WindowManager. Please use an Activity or a ContextWrapper around one instead.`
|
||||
* @param activitySize The set to {@link true}, then size returned will be that of the activity
|
||||
* and can be smaller than physical display size in multi-window mode.
|
||||
* @return Returns the display size as {@link Point}.
|
||||
*/
|
||||
public static Point getDisplaySize( @NonNull Context context, boolean activitySize) {
|
||||
// android.view.WindowManager.getDefaultDisplay() and Display.getSize() are deprecated in
|
||||
// API 30 and give wrong values in API 30 for activitySize=false in multi-window
|
||||
androidx.window.WindowManager windowManager = new androidx.window.WindowManager(context);
|
||||
androidx.window.WindowMetrics windowMetrics;
|
||||
if (activitySize)
|
||||
windowMetrics = windowManager.getCurrentWindowMetrics();
|
||||
else
|
||||
windowMetrics = windowManager.getMaximumWindowMetrics();
|
||||
return new Point(windowMetrics.getBounds().width(), windowMetrics.getBounds().height());
|
||||
}
|
||||
|
||||
/** Convert {@link Rect} to {@link String}. */
|
||||
public static String toRectString(Rect rect) {
|
||||
if (rect == null) return "null";
|
||||
return "(" + rect.left + "," + rect.top + "), (" + rect.right + "," + rect.bottom + ")";
|
||||
}
|
||||
|
||||
/** Convert {@link Point} to {@link String}. */
|
||||
public static String toPointString(Point point) {
|
||||
if (point == null) return "null";
|
||||
return "(" + point.x + "," + point.y + ")";
|
||||
}
|
||||
|
||||
/** Get the {@link Activity} associated with the {@link Context} if available. */
|
||||
@Nullable
|
||||
public static Activity getActivity(Context context) {
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof Activity) {
|
||||
return (Activity)context;
|
||||
}
|
||||
context = ((ContextWrapper)context).getBaseContext();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Convert value in device independent pixels (dp) to pixels (px) units. */
|
||||
public static int dpToPx(Context context, int dp) {
|
||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
|
||||
}
|
||||
|
||||
|
||||
public static void setLayoutMarginsInDp(@NonNull View view, int left, int top, int right, int bottom) {
|
||||
Context context = view.getContext();
|
||||
setLayoutMarginsInPixels(view, dpToPx(context, left), dpToPx(context, top), dpToPx(context, right), dpToPx(context, bottom));
|
||||
}
|
||||
|
||||
public static void setLayoutMarginsInPixels(@NonNull View view, int left, int top, int right, int bottom) {
|
||||
if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
|
||||
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
params.setMargins(left, top, right, bottom);
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
5
termux-shared/src/main/res/drawable/ic_copy.xml
Normal file
5
termux-shared/src/main/res/drawable/ic_copy.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/>
|
||||
</vector>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<!--
|
||||
Updated notification icon compliant with system icons guidelines
|
||||
https://material.io/design/iconography/system-icons.html
|
||||
-->
|
||||
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h24v24h-24z"/>
|
||||
|
||||
<path
|
||||
android:pathData="M5,4H2L8,12L2,20H5L11,12L5,4Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
<path
|
||||
android:pathData="M19.59,14
|
||||
l-2.09,2.09
|
||||
L15.41,14
|
||||
L14,15.41
|
||||
l2.09,2.09
|
||||
L14,19.59
|
||||
L15.41,21
|
||||
l2.09,-2.08
|
||||
L19.59,21
|
||||
L21,19.59
|
||||
l-2.08,-2.09
|
||||
L21,15.41
|
||||
L19.59,14
|
||||
z"
|
||||
android:fillColor="#ffffff"/>
|
||||
|
||||
</group>
|
||||
</vector>
|
||||
5
termux-shared/src/main/res/drawable/ic_share.xml
Normal file
5
termux-shared/src/main/res/drawable/ic_share.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#FFFFFF"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
|
||||
</vector>
|
||||
21
termux-shared/src/main/res/layout/activity_report.xml
Normal file
21
termux-shared/src/main/res/layout/activity_report.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_toolbar"
|
||||
android:id="@+id/partial_toolbar"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:overScrollMode="never"
|
||||
android:paddingTop="@dimen/content_padding"
|
||||
android:paddingBottom="36dip" />
|
||||
|
||||
</LinearLayout>
|
||||
93
termux-shared/src/main/res/layout/activity_text_io.xml
Normal file
93
termux-shared/src/main/res/layout/activity_text_io.xml
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
layout="@layout/partial_toolbar"
|
||||
android:id="@+id/partial_toolbar"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_io_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="@dimen/content_padding"
|
||||
android:gravity="start|center_vertical"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/black"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/text_io_label_separator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginHorizontal="@dimen/content_padding"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:visibility="invisible" />
|
||||
|
||||
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<!-- id must be assigned to scroll views to restore scroll position automatically on activity resume -->
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/text_io_nested_scroll_view"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:padding="@dimen/content_padding_half"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@+id/text_io_text_character_usage"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/text_io_horizontal_scroll_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/text_io_text_linear_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/text_io_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="start|top"
|
||||
android:inputType="textMultiLine"
|
||||
android:importantForAutofill="no"
|
||||
tools:ignore="LabelFor" />
|
||||
</LinearLayout>
|
||||
|
||||
</HorizontalScrollView>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_io_text_character_usage"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingEnd="@dimen/content_padding_half"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:gravity="end|center_vertical"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/black"
|
||||
android:visibility="invisible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
50
termux-shared/src/main/res/layout/dialog_show_message.xml
Normal file
50
termux-shared/src/main/res/layout/dialog_show_message.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/red_400"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp">
|
||||
<TextView
|
||||
android:id="@+id/dialog_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Title"
|
||||
android:textColor="@android:color/white"/>
|
||||
</LinearLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" >
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:scrollbars="vertical">
|
||||
<TextView
|
||||
android:id="@+id/dialog_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="web|email"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/tab_indicator_text"
|
||||
android:textColorLink="@android:color/black"/>
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:fillViewport="true"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingRight="16dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/code_text_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/background_markdown_code_block"
|
||||
android:fontFamily="monospace"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/default_text_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginLeft="16dip"
|
||||
android:layout_marginRight="16dip"
|
||||
android:breakStrategy="simple"
|
||||
android:hyphenationFrequency="none"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="#000"
|
||||
android:textSize="12sp" />
|
||||
22
termux-shared/src/main/res/layout/partial_toolbar.xml
Normal file
22
termux-shared/src/main/res/layout/partial_toolbar.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/toolbar_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimaryDark"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
|
||||
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
|
||||
app:titleTextAppearance="@style/Toolbar.Title">
|
||||
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
</LinearLayout>
|
||||
20
termux-shared/src/main/res/menu/menu_report.xml
Normal file
20
termux-shared/src/main/res/menu/menu_report.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_item_share_report"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/action_share"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_item_copy_report"
|
||||
android:icon="@drawable/ic_copy"
|
||||
android:title="@string/action_copy"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_item_save_report_to_file"
|
||||
android:title="@string/action_save_to_file"
|
||||
app:showAsAction="never" />
|
||||
</menu>
|
||||
20
termux-shared/src/main/res/menu/menu_text_io.xml
Normal file
20
termux-shared/src/main/res/menu/menu_text_io.xml
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_item_cancel"
|
||||
android:title="@string/action_cancel"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_item_share_text"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/action_share"
|
||||
app:showAsAction="never" />
|
||||
<item
|
||||
android:id="@+id/menu_item_copy_text"
|
||||
android:icon="@drawable/ic_copy"
|
||||
android:title="@string/action_copy"
|
||||
app:showAsAction="never" />
|
||||
|
||||
</menu>
|
||||
59
termux-shared/src/main/res/raw/apt_info_script.sh
Normal file
59
termux-shared/src/main/res/raw/apt_info_script.sh
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
#!/bin/bash
|
||||
|
||||
subscribed_repositories() {
|
||||
local main_sources
|
||||
main_sources=$(grep -P '^\s*deb\s' "@TERMUX_PREFIX@/etc/apt/sources.list")
|
||||
|
||||
if [ -n "$main_sources" ]; then
|
||||
echo "#### sources.list"
|
||||
echo "\`$main_sources\`"
|
||||
fi
|
||||
|
||||
local filename repo_package supl_sources
|
||||
while read -r filename; do
|
||||
repo_package=$(dpkg -S "$filename" 2>/dev/null | cut -d : -f 1)
|
||||
supl_sources=$(grep -P '^\s*deb\s' "$filename")
|
||||
|
||||
if [ -n "$supl_sources" ]; then
|
||||
if [ -n "$repo_package" ]; then
|
||||
echo "#### $repo_package (sources.list.d/$(basename "$filename"))"
|
||||
else
|
||||
echo "#### sources.list.d/$(basename "$filename")"
|
||||
fi
|
||||
echo "\`$supl_sources\` "
|
||||
fi
|
||||
done < <(find "@TERMUX_PREFIX@/etc/apt/sources.list.d" -maxdepth 1 ! -type d)
|
||||
}
|
||||
|
||||
updatable_packages() {
|
||||
local updatable
|
||||
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
echo "Running as root. Cannot check updatable packages."
|
||||
else
|
||||
apt update >/dev/null 2>&1
|
||||
updatable=$(apt list --upgradable 2>/dev/null | tail -n +2)
|
||||
|
||||
if [ -z "$updatable" ];then
|
||||
echo "All packages up to date"
|
||||
else
|
||||
echo $'```\n'"$updatable"$'\n```\n'
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
output="
|
||||
### Subscribed Repositories
|
||||
|
||||
$(subscribed_repositories)
|
||||
##
|
||||
|
||||
|
||||
### Updatable Packages
|
||||
|
||||
$(updatable_packages)
|
||||
##
|
||||
|
||||
"
|
||||
|
||||
echo "$output"
|
||||
BIN
termux-shared/src/main/res/raw/bell.ogg
Normal file
BIN
termux-shared/src/main/res/raw/bell.ogg
Normal file
Binary file not shown.
19
termux-shared/src/main/res/values-night/themes.xml
Executable file
19
termux-shared/src/main/res/values-night/themes.xml
Executable file
|
|
@ -0,0 +1,19 @@
|
|||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.MaterialComponents.DayNight.TermuxPrimaryActivity" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/red_400</item>
|
||||
<item name="colorPrimaryVariant">@color/red_800</item>
|
||||
<item name="colorOnPrimary">@color/black</item>
|
||||
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/grey_900</item>
|
||||
<item name="colorSecondaryVariant">@color/black</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
|
||||
<!-- Text color. -->
|
||||
<item name="android:textColorLink">@color/grey_200</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
termux-shared/src/main/res/values/colors.xml
Normal file
18
termux-shared/src/main/res/values/colors.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="background_markdown_code_inline">#1F000000</color>
|
||||
<color name="background_markdown_code_block">#0F000000</color>
|
||||
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
|
||||
<color name="red_400">#FF0000</color>
|
||||
<color name="red_800">#C4001D</color>
|
||||
|
||||
<color name="grey_200">#EEEEEE</color>
|
||||
<color name="grey_800">#424242</color>
|
||||
<color name="grey_900">#212121</color>
|
||||
|
||||
<color name="red_error">#DC143C</color>
|
||||
<color name="red_error_link">#FC143C</color>
|
||||
</resources>
|
||||
10
termux-shared/src/main/res/values/dimens.xml
Normal file
10
termux-shared/src/main/res/values/dimens.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||
|
||||
<dimen name="content_padding">8dip</dimen>
|
||||
<dimen name="content_padding_double">16dip</dimen>
|
||||
<dimen name="content_padding_half">4dip</dimen>
|
||||
</resources>
|
||||
118
termux-shared/src/main/res/values/strings.xml
Normal file
118
termux-shared/src/main/res/values/strings.xml
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?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">
|
||||
<!ENTITY TERMUX_PREFIX_DIR_PATH "/data/data/com.termux/files/usr">
|
||||
]>
|
||||
|
||||
<resources>
|
||||
|
||||
<string name="msg_directory_absolute_path">%1$s Directory Absolute Path: \"%2$s\"</string>
|
||||
|
||||
|
||||
|
||||
<!-- PackageUtils -->
|
||||
<string name="error_app_not_installed_or_disabled_warning">The %1$s (%2$s) app is not installed or is disabled."</string>
|
||||
<string name="error_get_package_context_failed_title">Failed To Get Package Context</string>
|
||||
<string name="error_get_package_context_failed_message">Failed to get package context for the \"%1$s\" package.
|
||||
This may be because the app package is not installed or it has different APK signature from the current app.
|
||||
Check install instruction at %2$s for more details.</string>
|
||||
<string name="error_get_component_state_failed">Failed to get %1$s/%2$s component state"</string>
|
||||
<string name="error_enable_component_failed">Failed to enable %1$s/%2$s component"</string>
|
||||
<string name="error_disable_component_failed">Failed to enable %1$s/%2$s component"</string>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- PermissionUtils -->
|
||||
<string name="message_sudo_please_grant_permissions">Please grant permissions on next screen</string>
|
||||
|
||||
|
||||
|
||||
<!-- ReportActivity -->
|
||||
<string name="title_report_text">Report Text</string>
|
||||
<string name="msg_report_truncated">**Report Truncated**\n\nReport is too large to view here.
|
||||
Use `Save To File` option from options menu (3-dots on top right) and view it in an external text editor app.\n\n##\n\n</string>
|
||||
|
||||
|
||||
|
||||
<!-- ShareUtils -->
|
||||
<string name="title_share_with">Share With</string>
|
||||
<string name="title_open_url_with">Open URL With</string>
|
||||
<string name="msg_storage_permission_not_granted">The storage permission not granted."</string>
|
||||
<string name="msg_file_saved_successfully">The %1$s file saved successfully at \"%2$s\""</string>
|
||||
|
||||
|
||||
|
||||
<!-- ShellUtils -->
|
||||
<string name="error_sending_sigkill_to_process">Sending SIGKILL to process on user request or because android is killing the execution service</string>
|
||||
<string name="error_execution_cancelled">Execution has been cancelled since execution service is being killed</string>
|
||||
<string name="error_failed_to_execute_termux_session_command">"Failed to execute \"%1$s\" termux session command"</string>
|
||||
<string name="error_failed_to_execute_termux_task_command">"Failed to execute \"%1$s\" termux task command"</string>
|
||||
<string name="error_exception_received_while_executing_termux_session_command">Exception received while to executing \"%1$s\" termux session command.\nException: %2$s</string>
|
||||
<string name="error_exception_received_while_executing_termux_task_command">Exception received while to executing \"%1$s\" termux task command.\nException: %2$s"</string>
|
||||
|
||||
|
||||
|
||||
<!-- TermuxUtils -->
|
||||
<string name="msg_report_issue">
|
||||
If you want to report this issue, then copy its text from the options menu (3-dots on top right) and post
|
||||
an issue on one of the following links.
|
||||
\n\nIf you are posting a Termux app crash report, then please provide details on what you were doing that
|
||||
caused the crash and how to reproduce it, if possible.
|
||||
\n\nIf you are posting an issue on Github, then post it in the repository at which the report belongs at.
|
||||
Issues opened or emails sent with **(partial) screenshots** instead of copied text or a file of this report
|
||||
**will likely be automatically closed/deleted**. You may optionally remove any device specific info that
|
||||
you consider private or don\'t want to share or that is not relevant to the issue.
|
||||
\n\nWe do not provide support for any hacking related tools/scripts. Any questions asked about them over email,
|
||||
on github or other official termux community forums **will likely be automatically closed/deleted** and may
|
||||
even result in **temporary or permanent** ban. Check %1$s/wiki/Hacking for details.</string>
|
||||
|
||||
<string name="msg_termux_app_required_by_app">The &TERMUX_APP_NAME; is required by the %1$s app to run termux commands."</string>
|
||||
<string name="error_termux_app_package_context_not_accessible">The &TERMUX_APP_NAME; app (package context) is not accessible."</string>
|
||||
<string name="error_termux_prefix_dir_path_not_accessible">The &TERMUX_APP_NAME; app $PREFIX directory is not accessible by the %1$s app.
|
||||
This may be because you have not installed or setup &TERMUX_APP_NAME; app or
|
||||
&TERMUX_APP_NAME; app and %1$s app both have different APK signatures because you have managed to install both apps from different sources.
|
||||
It may also be because &TERMUX_APP_NAME; $PREFIX directory \"&TERMUX_PREFIX_DIR_PATH;\" does not exist or does not have read,
|
||||
write and execute permissions."</string>
|
||||
|
||||
|
||||
|
||||
<!-- Miscellaneous -->
|
||||
<string name="action_yes">Yes</string>
|
||||
<string name="action_no">No</string>
|
||||
<string name="action_copy">Copy</string>
|
||||
<string name="action_share">Share</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_save_to_file">Save To File</string>
|
||||
|
||||
|
||||
|
||||
<!-- Launcher Icons -->
|
||||
<string name="action_enable_launcher_icon">Enable Launcher Icon</string>
|
||||
<string name="action_disable_launcher_icon">Disable Launcher Icon</string>
|
||||
<string name="msg_enabling_launcher_icon">Enabling %1$s app launcher icon"</string>
|
||||
<string name="msg_disabling_launcher_icon">Disabling %1$s app launcher icon"</string>
|
||||
<string name="setting_launcher_icon_title">Launcher Icon Enabled</string>
|
||||
<string name="setting_launcher_icon_enabled_off">Launcher Icon will be disabled.</string>
|
||||
<string name="setting_launcher_icon_enabled_on">Launcher Icon will be enabled. (Default)</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>
|
||||
42
termux-shared/src/main/res/values/themes.xml
Normal file
42
termux-shared/src/main/res/values/themes.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.MaterialComponents.DayNight.TermuxPrimaryActivity" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/red_400</item>
|
||||
<item name="colorPrimaryVariant">@color/red_800</item>
|
||||
<item name="colorOnPrimary">@color/white</item>
|
||||
|
||||
<!-- Secondary brand color. -->
|
||||
<item name="colorSecondary">@color/grey_900</item>
|
||||
<item name="colorSecondaryVariant">@color/black</item>
|
||||
<item name="colorOnSecondary">@color/white</item>
|
||||
|
||||
<!-- Status bar color. -->
|
||||
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||
|
||||
<!-- Text color. -->
|
||||
<item name="android:textColorPrimary">@color/black</item>
|
||||
<item name="android:textColorLink">@color/grey_800</item>
|
||||
<item name="android:textColorSecondary">@color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.AppCompat.TermuxReportActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimaryDark">#FF0000</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.AppCompat.TermuxTextIOActivity" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="colorPrimaryDark">#FF0000</item>
|
||||
</style>
|
||||
|
||||
<style name="Toolbar.Title" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="ViewDivider">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">1dp</item>
|
||||
<item name="android:layout_marginTop">@dimen/activity_vertical_margin</item>
|
||||
<item name="android:layout_marginBottom">@dimen/activity_vertical_margin</item>
|
||||
<item name="android:background">?android:attr/listDivider</item>
|
||||
</style>
|
||||
</resources>
|
||||
Loading…
Add table
Add a link
Reference in a new issue