From c3280a94f0b9c7d7c64d5a538fbb50132831acc4 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Sat, 11 Sep 2021 13:54:32 +0500 Subject: [PATCH] Added: Add TextIOActivity and TextIOInfo The `TextIOActivity` can be used to edit or view text based on various config options defined by `TextIOInfo` and supports `monospace` font and horizontal scrolling for editing scripts, etc. Current max text limit is `95KB`, which can be increased in future. --- termux-shared/build.gradle | 1 + .../shared/activities/TextIOActivity.java | 278 ++++++++++++++++++ .../com/termux/shared/models/TextIOInfo.java | 234 +++++++++++++++ .../src/main/res/layout/activity_text_io.xml | 93 ++++++ .../src/main/res/menu/menu_text_io.xml | 20 ++ termux-shared/src/main/res/values/strings.xml | 7 +- termux-shared/src/main/res/values/styles.xml | 4 + 7 files changed, 634 insertions(+), 3 deletions(-) create mode 100644 termux-shared/src/main/java/com/termux/shared/activities/TextIOActivity.java create mode 100644 termux-shared/src/main/java/com/termux/shared/models/TextIOInfo.java create mode 100644 termux-shared/src/main/res/layout/activity_text_io.xml create mode 100644 termux-shared/src/main/res/menu/menu_text_io.xml 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 + +