diff --git a/termux-shared/build.gradle b/termux-shared/build.gradle
index 2d778d93..a4891703 100644
--- a/termux-shared/build.gradle
+++ b/termux-shared/build.gradle
@@ -9,6 +9,7 @@ android {
implementation "androidx.annotation:annotation:1.2.0"
implementation "androidx.core:core:1.6.0-rc01"
implementation "androidx.window:window:1.0.0-alpha09"
+ 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"
diff --git a/termux-shared/src/main/java/com/termux/shared/activities/TextIOActivity.java b/termux-shared/src/main/java/com/termux/shared/activities/TextIOActivity.java
new file mode 100644
index 00000000..7cdf6ab0
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/activities/TextIOActivity.java
@@ -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 ` ` }
+ */
+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;
+ }
+
+}
diff --git a/termux-shared/src/main/java/com/termux/shared/models/TextIOInfo.java b/termux-shared/src/main/java/com/termux/shared/models/TextIOInfo.java
new file mode 100644
index 00000000..df9993af
--- /dev/null
+++ b/termux-shared/src/main/java/com/termux/shared/models/TextIOInfo.java
@@ -0,0 +1,234 @@
+package com.termux.shared.models;
+
+import android.graphics.Color;
+import android.graphics.Typeface;
+
+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 {
+
+ 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;
+ }
+
+}
diff --git a/termux-shared/src/main/res/layout/activity_text_io.xml b/termux-shared/src/main/res/layout/activity_text_io.xml
new file mode 100644
index 00000000..3754b9f5
--- /dev/null
+++ b/termux-shared/src/main/res/layout/activity_text_io.xml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/termux-shared/src/main/res/menu/menu_text_io.xml b/termux-shared/src/main/res/menu/menu_text_io.xml
new file mode 100644
index 00000000..192ddad0
--- /dev/null
+++ b/termux-shared/src/main/res/menu/menu_text_io.xml
@@ -0,0 +1,20 @@
+
+
diff --git a/termux-shared/src/main/res/values/strings.xml b/termux-shared/src/main/res/values/strings.xml
index eda0a417..070b1870 100644
--- a/termux-shared/src/main/res/values/strings.xml
+++ b/termux-shared/src/main/res/values/strings.xml
@@ -33,9 +33,6 @@
- Copy
- Share
- Save To File
Report Text
**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
@@ -87,6 +84,10 @@
Yes
No
+ Copy
+ Share
+ Cancel
+ Save To File
diff --git a/termux-shared/src/main/res/values/styles.xml b/termux-shared/src/main/res/values/styles.xml
index 1541ea39..b6c7a87a 100644
--- a/termux-shared/src/main/res/values/styles.xml
+++ b/termux-shared/src/main/res/values/styles.xml
@@ -4,6 +4,10 @@
- #FF0000
+
+