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.
This commit is contained in:
agnostic-apollo
2021-09-11 13:54:32 +05:00
parent 5f3b1ccf90
commit c3280a94f0
7 changed files with 634 additions and 3 deletions

View File

@@ -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"

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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>

View 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>

View File

@@ -33,9 +33,6 @@
<!-- ReportActivity -->
<string name="action_copy">Copy</string>
<string name="action_share">Share</string>
<string name="action_save_to_file">Save To File</string>
<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>
@@ -87,6 +84,10 @@
<!-- 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>

View File

@@ -4,6 +4,10 @@
<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>