From 3bb2849a88f278a780f42ad31baabba2adc6fa86 Mon Sep 17 00:00:00 2001 From: David Kramer Date: Mon, 18 Jun 2018 13:06:34 -0600 Subject: [PATCH 001/912] Sends broadcast on app open to notify addon termux receivers --- .../java/com/termux/app/TermuxActivity.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index d50ab4304e..9478ca7cf0 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -17,6 +17,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; @@ -104,6 +105,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style"; + private static final String BROADCAST_TERMUX_OPENED = "com.termux.app.OPENED"; + /** The main view of the activity showing the terminal. Initialized in onCreate(). */ @SuppressWarnings("NullableProblems") @NonNull @@ -334,6 +337,26 @@ public boolean onLongClick(View v) { checkForFontAndColors(); mBellSoundId = mBellSoundPool.load(this, R.raw.bell, 1); + + sendOpenedBroadcast(); + } + + /** + * Send a broadcast notifying Termux app has been opened + */ + void sendOpenedBroadcast() { + Intent broadcast = new Intent(BROADCAST_TERMUX_OPENED); + List matches = 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); + sendBroadcast(explicitBroadcast); + } } void toggleShowExtraKeys() { From 35a4fdacbe4d75bae0d648cf66a6526d0daee313 Mon Sep 17 00:00:00 2001 From: mao Date: Sat, 5 Oct 2019 18:05:42 +0800 Subject: [PATCH 002/912] Add selection mode cursor controller --- .../java/com/termux/view/TerminalView.java | 741 +++++++++++++++--- 1 file changed, 635 insertions(+), 106 deletions(-) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 9ec398046b..2065248779 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -8,14 +8,13 @@ import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Typeface; -import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.os.Build; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Log; -import android.view.accessibility.AccessibilityManager; import android.view.ActionMode; import android.view.HapticFeedbackConstants; import android.view.InputDevice; @@ -25,9 +24,16 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.ViewTreeObserver; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityManager; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; +import android.widget.PopupWindow; import android.widget.Scroller; import com.termux.terminal.EmulatorDebug; @@ -54,11 +60,14 @@ public final class TerminalView extends View { /** The top row of text to display. Ranges from -activeTranscriptRows to 0. */ int mTopRow; - boolean mIsSelectingText = false, mIsDraggingLeftSelection, mInitialTextSelection; + boolean mIsSelectingText = false; int mSelX1 = -1, mSelX2 = -1, mSelY1 = -1, mSelY2 = -1; - float mSelectionDownX, mSelectionDownY; private ActionMode mActionMode; - private BitmapDrawable mLeftSelectionHandle, mRightSelectionHandle; + Drawable mSelectHandleLeft; + Drawable mSelectHandleRight; + final int[] mTempCoords = new int[2]; + Rect mTempRect; + private SelectionModifierCursorController mSelectionModifierCursorController; float mScaleFactor = 1.f; final GestureAndScaleRecognizer mGestureRecognizer; @@ -102,7 +111,7 @@ public boolean onUp(MotionEvent e) { public boolean onSingleTapUp(MotionEvent e) { if (mEmulator == null) return true; if (mIsSelectingText) { - toggleSelectingText(null); + stopTextSelectionMode(); return true; } requestFocus(); @@ -117,7 +126,7 @@ public boolean onSingleTapUp(MotionEvent e) { @Override public boolean onScroll(MotionEvent e, float distanceX, float distanceY) { - if (mEmulator == null || mIsSelectingText) return true; + if (mEmulator == null) return true; if (mEmulator.isMouseTrackingActive() && e.isFromSource(InputDevice.SOURCE_MOUSE)) { // If moving with mouse pointer while pressing button, report that instead of scroll. // This means that we never report moving with button press-events for touch input, @@ -195,7 +204,7 @@ public void onLongPress(MotionEvent e) { if (mClient.onLongPress(e)) return; if (!mIsSelectingText) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - toggleSelectingText(e); + startSelectingText(e); } } }); @@ -368,7 +377,7 @@ public void onScreenUpdated() { if (-mTopRow + rowShift > rowsInHistory) { // .. unless we're hitting the end of history transcript, in which // case we abort text selection and scroll to end. - toggleSelectingText(null); + stopTextSelectionMode(); } else { skipScrolling = true; mTopRow -= rowShift; @@ -475,56 +484,7 @@ public boolean onTouchEvent(MotionEvent ev) { final int action = ev.getAction(); if (mIsSelectingText) { - int cy = (int) (ev.getY() / mRenderer.mFontLineSpacing) + mTopRow; - int cx = (int) (ev.getX() / mRenderer.mFontWidth); - - switch (action) { - case MotionEvent.ACTION_UP: - mInitialTextSelection = false; - break; - case MotionEvent.ACTION_DOWN: - int distanceFromSel1 = Math.abs(cx - mSelX1) + Math.abs(cy - mSelY1); - int distanceFromSel2 = Math.abs(cx - mSelX2) + Math.abs(cy - mSelY2); - mIsDraggingLeftSelection = distanceFromSel1 <= distanceFromSel2; - mSelectionDownX = ev.getX(); - mSelectionDownY = ev.getY(); - break; - case MotionEvent.ACTION_MOVE: - if (mInitialTextSelection) break; - float deltaX = ev.getX() - mSelectionDownX; - float deltaY = ev.getY() - mSelectionDownY; - int deltaCols = (int) Math.ceil(deltaX / mRenderer.mFontWidth); - int deltaRows = (int) Math.ceil(deltaY / mRenderer.mFontLineSpacing); - mSelectionDownX += deltaCols * mRenderer.mFontWidth; - mSelectionDownY += deltaRows * mRenderer.mFontLineSpacing; - if (mIsDraggingLeftSelection) { - mSelX1 += deltaCols; - mSelY1 += deltaRows; - } else { - mSelX2 += deltaCols; - mSelY2 += deltaRows; - } - - mSelX1 = Math.min(mEmulator.mColumns, Math.max(0, mSelX1)); - mSelX2 = Math.min(mEmulator.mColumns, Math.max(0, mSelX2)); - - if (mSelY1 == mSelY2 && mSelX1 > mSelX2 || mSelY1 > mSelY2) { - // Switch handles. - mIsDraggingLeftSelection = !mIsDraggingLeftSelection; - int tmpX1 = mSelX1, tmpY1 = mSelY1; - mSelX1 = mSelX2; - mSelY1 = mSelY2; - mSelX2 = tmpX1; - mSelY2 = tmpY1; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - mActionMode.invalidateContentRect(); - invalidate(); - break; - default: - break; - } + updateFloatingToolbarVisibility(ev); mGestureRecognizer.onTouchEvent(ev); return true; } else if (ev.isFromSource(InputDevice.SOURCE_MOUSE)) { @@ -562,7 +522,7 @@ public boolean onKeyPreIme(int keyCode, KeyEvent event) { Log.i(EmulatorDebug.LOG_TAG, "onKeyPreIme(keyCode=" + keyCode + ", event=" + event + ")"); if (keyCode == KeyEvent.KEYCODE_BACK) { if (mIsSelectingText) { - toggleSelectingText(null); + stopTextSelectionMode(); return true; } else if (mClient.shouldBackButtonBeMappedToEscape()) { // Intercept back button to treat it as escape: @@ -771,59 +731,439 @@ protected void onDraw(Canvas canvas) { } else { mRenderer.render(mEmulator, canvas, mTopRow, mSelY1, mSelY2, mSelX1, mSelX2); - if (mIsSelectingText) { - final int gripHandleWidth = mLeftSelectionHandle.getIntrinsicWidth(); - final int gripHandleMargin = gripHandleWidth / 4; // See the png. - - int right = Math.round((mSelX1) * mRenderer.mFontWidth) + gripHandleMargin; - int top = (mSelY1 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent; - mLeftSelectionHandle.setBounds(right - gripHandleWidth, top, right, top + mLeftSelectionHandle.getIntrinsicHeight()); - mLeftSelectionHandle.draw(canvas); - int left = Math.round((mSelX2 + 1) * mRenderer.mFontWidth) - gripHandleMargin; - top = (mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing + mRenderer.mFontLineSpacingAndAscent; - mRightSelectionHandle.setBounds(left, top, left + gripHandleWidth, top + mRightSelectionHandle.getIntrinsicHeight()); - mRightSelectionHandle.draw(canvas); + SelectionModifierCursorController selectionController = getSelectionController(); + if (selectionController != null && selectionController.isActive()) { + selectionController.updatePosition(); } } } /** Toggle text selection mode in the view. */ @TargetApi(23) - public void toggleSelectingText(MotionEvent ev) { - mIsSelectingText = !mIsSelectingText; - mClient.copyModeChanged(mIsSelectingText); + public void startSelectingText(MotionEvent ev) { + int cx = (int) (ev.getX() / mRenderer.mFontWidth); + final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE); + // Offset for finger: + final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; + int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow; + + mSelX1 = mSelX2 = cx; + mSelY1 = mSelY2 = cy; + + TerminalBuffer screen = mEmulator.getScreen(); + if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { + // Selecting something other than whitespace. Expand to word. + while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) { + mSelX1--; + } + while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) { + mSelX2++; + } + } + startTextSelectionMode(); + } - if (mIsSelectingText) { - if (mLeftSelectionHandle == null) { - mLeftSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_left_material); - mRightSelectionHandle = (BitmapDrawable) getContext().getDrawable(R.drawable.text_select_handle_right_material); + public TerminalSession getCurrentSession() { + return mTermSession; + } + + private CharSequence getText() { + return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow + mEmulator.mRows); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + if (mSelectionModifierCursorController != null) { + getViewTreeObserver().addOnTouchModeChangeListener(mSelectionModifierCursorController); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + + if (mSelectionModifierCursorController != null) { + getViewTreeObserver().removeOnTouchModeChangeListener(mSelectionModifierCursorController); + mSelectionModifierCursorController.onDetached(); + } + } + + + private int getCursorX(float x) { + return (int) (x / mRenderer.mFontWidth); + } + + private int getCursorY(float y) { + return (int) (((y - 40) / mRenderer.mFontLineSpacing) + mTopRow); + } + + private int getPointX(int cx) { + if (cx > mEmulator.mColumns) { + cx = mEmulator.mColumns; + } + return Math.round(cx * mRenderer.mFontWidth); + } + + private int getPointY(int cy) { + return Math.round((cy - mTopRow) * mRenderer.mFontLineSpacing); + } + + /** + * A CursorController instance can be used to control a cursor in the text. + * It is not used outside of {@link TerminalView}. + */ + private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { + /** + * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}. + * See also {@link #hide()}. + */ + void show(); + + /** + * Hide the cursor controller from screen. + * See also {@link #show()}. + */ + void hide(); + + /** + * @return true if the CursorController is currently visible + */ + boolean isActive(); + + /** + * Update the controller's position. + */ + void updatePosition(HandleView handle, int x, int y); + + void updatePosition(); + + /** + * This method is called by {@link #onTouchEvent(MotionEvent)} and gives the controller + * a chance to become active and/or visible. + * + * @param event The touch event + */ + boolean onTouchEvent(MotionEvent event); + + /** + * Called when the view is detached from window. Perform house keeping task, such as + * stopping Runnable thread that would otherwise keep a reference on the context, thus + * preventing the activity to be recycled. + */ + void onDetached(); + } + + private class HandleView extends View { + private Drawable mDrawable; + private PopupWindow mContainer; + private int mPointX; + private int mPointY; + private CursorController mController; + private boolean mIsDragging; + private float mTouchToWindowOffsetX; + private float mTouchToWindowOffsetY; + private float mHotspotX; + private float mHotspotY; + private float mTouchOffsetY; + private int mLastParentX; + private int mLastParentY; + + int mHandleWidth; + private final int mOrigOrient; + private int mOrientation; + + + public static final int LEFT = 0; + public static final int RIGHT = 2; + private int mHandleHeight; + + public HandleView(CursorController controller, int orientation) { + super(TerminalView.this.getContext()); + mController = controller; + mContainer = new PopupWindow(TerminalView.this.getContext(), null, + android.R.attr.textSelectHandleWindowStyle); + mContainer.setSplitTouchEnabled(true); + mContainer.setClippingEnabled(false); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); } + mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); + mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); - int cx = (int) (ev.getX() / mRenderer.mFontWidth); - final boolean eventFromMouse = ev.isFromSource(InputDevice.SOURCE_MOUSE); - // Offset for finger: - final int SELECT_TEXT_OFFSET_Y = eventFromMouse ? 0 : -40; - int cy = (int) ((ev.getY() + SELECT_TEXT_OFFSET_Y) / mRenderer.mFontLineSpacing) + mTopRow; + this.mOrigOrient = orientation; + setOrientation(orientation); + } + + public void setOrientation(int orientation) { + mOrientation = orientation; + int handleWidth = 0; + switch (orientation) { + case LEFT: { + if (mSelectHandleLeft == null) { - mSelX1 = mSelX2 = cx; - mSelY1 = mSelY2 = cy; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mSelectHandleLeft = getContext().getDrawable( + R.drawable.text_select_handle_left_material); + } else { + mSelectHandleLeft = getContext().getResources().getDrawable( + R.drawable.text_select_handle_left_material); - TerminalBuffer screen = mEmulator.getScreen(); - if (!" ".equals(screen.getSelectedText(mSelX1, mSelY1, mSelX1, mSelY1))) { - // Selecting something other than whitespace. Expand to word. - while (mSelX1 > 0 && !"".equals(screen.getSelectedText(mSelX1 - 1, mSelY1, mSelX1 - 1, mSelY1))) { - mSelX1--; + } + } + // + mDrawable = mSelectHandleLeft; + handleWidth = mDrawable.getIntrinsicWidth(); + mHotspotX = (handleWidth * 3) / 4; + break; } - while (mSelX2 < mEmulator.mColumns - 1 && !"".equals(screen.getSelectedText(mSelX2 + 1, mSelY1, mSelX2 + 1, mSelY1))) { - mSelX2++; + + case RIGHT: { + if (mSelectHandleRight == null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + mSelectHandleRight = getContext().getDrawable( + R.drawable.text_select_handle_right_material); + } else { + mSelectHandleRight = getContext().getResources().getDrawable( + R.drawable.text_select_handle_right_material); + } + } + mDrawable = mSelectHandleRight; + handleWidth = mDrawable.getIntrinsicWidth(); + mHotspotX = handleWidth / 4; + break; } + + } + + mHandleHeight = mDrawable.getIntrinsicHeight(); + + mHandleWidth = handleWidth; + mTouchOffsetY = -mHandleHeight * 0.3f; + mHotspotY = 0; + invalidate(); + } + + public void changeOrientation(int orientation) { + if (mOrientation != orientation) { + setOrientation(orientation); } + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(mDrawable.getIntrinsicWidth(), + mDrawable.getIntrinsicHeight()); + } + + public void show() { + if (!isPositionVisible()) { + hide(); + return; + } + mContainer.setContentView(this); + final int[] coords = mTempCoords; + TerminalView.this.getLocationInWindow(coords); + coords[0] += mPointX; + coords[1] += mPointY; + mContainer.showAtLocation(TerminalView.this, 0, coords[0], coords[1]); + } - mInitialTextSelection = true; - mIsDraggingLeftSelection = true; - mSelectionDownX = ev.getX(); - mSelectionDownY = ev.getY(); + public void hide() { + mIsDragging = false; + mContainer.dismiss(); + } + + public boolean isShowing() { + return mContainer.isShowing(); + } + + private void checkChangedOrientation() { + + final TerminalView hostView = TerminalView.this; + final int left = hostView.getLeft(); + final int right = hostView.getWidth(); + final int top = hostView.getTop(); + final int bottom = hostView.getHeight(); + + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect clip = mTempRect; + clip.left = left + TerminalView.this.getPaddingLeft(); + clip.top = top + TerminalView.this.getPaddingTop(); + clip.right = right - TerminalView.this.getPaddingRight(); + clip.bottom = bottom - TerminalView.this.getPaddingBottom(); + + final ViewParent parent = hostView.getParent(); + if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + return; + } + + final int[] coords = mTempCoords; + hostView.getLocationInWindow(coords); + final int posX = coords[0] + mPointX; + if (posX + (int) mHotspotX < clip.left) { + changeOrientation(RIGHT); + } else if (posX + mHandleWidth > clip.right) { + changeOrientation(LEFT); + } else { + changeOrientation(mOrigOrient); + } + } + + private boolean isPositionVisible() { + // Always show a dragging handle. + if (mIsDragging) { + return true; + } + + final TerminalView hostView = TerminalView.this; + final int left = 0; + final int right = hostView.getWidth(); + final int top = 0; + final int bottom = hostView.getHeight(); + + if (mTempRect == null) { + mTempRect = new Rect(); + } + final Rect clip = mTempRect; + clip.left = left + TerminalView.this.getPaddingLeft(); + clip.top = top + TerminalView.this.getPaddingTop(); + clip.right = right - TerminalView.this.getPaddingRight(); + clip.bottom = bottom - TerminalView.this.getPaddingBottom(); + + final ViewParent parent = hostView.getParent(); + if (parent == null || !parent.getChildVisibleRect(hostView, clip, null)) { + return false; + } + + final int[] coords = mTempCoords; + hostView.getLocationInWindow(coords); + final int posX = coords[0] + mPointX + (int) mHotspotX; + final int posY = coords[1] + mPointY + (int) mHotspotY; + + return posX >= clip.left && posX <= clip.right && + posY >= clip.top && posY <= clip.bottom; + } + + private void moveTo(int x, int y) { + mPointX = x; + mPointY = y; + checkChangedOrientation(); + if (isPositionVisible()) { + int[] coords = null; + if (mContainer.isShowing()) { + coords = mTempCoords; + TerminalView.this.getLocationInWindow(coords); + int x1 = coords[0] + mPointX; + int y1 = coords[1] + mPointY; + mContainer.update(x1, y1, + getWidth(), getHeight()); + } else { + show(); + } + + if (mIsDragging) { + if (coords == null) { + coords = mTempCoords; + TerminalView.this.getLocationInWindow(coords); + } + if (coords[0] != mLastParentX || coords[1] != mLastParentY) { + mTouchToWindowOffsetX += coords[0] - mLastParentX; + mTouchToWindowOffsetY += coords[1] - mLastParentY; + mLastParentX = coords[0]; + mLastParentY = coords[1]; + } + } + } else { + if (isShowing()) { + hide(); + } + } + } + + @Override + public void onDraw(Canvas c) { + final int drawWidth = mDrawable.getIntrinsicWidth(); + int height = mDrawable.getIntrinsicHeight(); + mDrawable.setBounds(0, 0, drawWidth, height); + mDrawable.draw(c); + + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouchEvent(MotionEvent ev) { + updateFloatingToolbarVisibility(ev); + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: { + final float rawX = ev.getRawX(); + final float rawY = ev.getRawY(); + mTouchToWindowOffsetX = rawX - mPointX; + mTouchToWindowOffsetY = rawY - mPointY; + final int[] coords = mTempCoords; + TerminalView.this.getLocationInWindow(coords); + mLastParentX = coords[0]; + mLastParentY = coords[1]; + mIsDragging = true; + break; + } + + case MotionEvent.ACTION_MOVE: { + final float rawX = ev.getRawX(); + final float rawY = ev.getRawY(); + + final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX; + final float newPosY = rawY - mTouchToWindowOffsetY + mHotspotY + mTouchOffsetY; + + mController.updatePosition(this, Math.round(newPosX), Math.round(newPosY)); + + + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsDragging = false; + } + return true; + } + + + public boolean isDragging() { + return mIsDragging; + } + + void positionAtCursor(final int cx, final int cy) { + int left = (int) (getPointX(cx) - mHotspotX); + int bottom = getPointY(cy + 1); + moveTo(left, bottom); + } + } + + + private class SelectionModifierCursorController implements CursorController { + private final int mHandleHeight; + // The cursor controller images + private HandleView mStartHandle, mEndHandle; + // Whether selection anchors are active + private boolean mIsShowing; + + SelectionModifierCursorController() { + mStartHandle = new HandleView(this, HandleView.LEFT); + mEndHandle = new HandleView(this, HandleView.RIGHT); + + mHandleHeight = Math.max(mStartHandle.mHandleHeight, mEndHandle.mHandleHeight); + } + + public void show() { + mIsShowing = true; + updatePosition(); + mStartHandle.show(); + mEndHandle.show(); final ActionMode.Callback callback = new ActionMode.Callback() { @Override @@ -865,7 +1205,7 @@ public boolean onActionItemClicked(ActionMode mode, MenuItem item) { showContextMenu(); break; } - toggleSelectingText(null); + stopTextSelectionMode(); return true; } @@ -874,7 +1214,6 @@ public void onDestroyActionMode(ActionMode mode) { } }; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mActionMode = startActionMode(new ActionMode.Callback2() { @Override @@ -903,28 +1242,218 @@ public void onGetContentRect(ActionMode mode, View view, Rect outRect) { int x2 = Math.round(mSelX2 * mRenderer.mFontWidth); int y1 = Math.round((mSelY1 - mTopRow) * mRenderer.mFontLineSpacing); int y2 = Math.round((mSelY2 + 1 - mTopRow) * mRenderer.mFontLineSpacing); - outRect.set(Math.min(x1, x2), y1, Math.max(x1, x2), y2); + + + if (x1 > x2) { + int tmp = x1; + x1 = x2; + x2 = tmp; + } + + outRect.set(x1, y1 + mHandleHeight, x2, y2 + mHandleHeight); } }, ActionMode.TYPE_FLOATING); } else { mActionMode = startActionMode(callback); } + } + + public void hide() { + mStartHandle.hide(); + mEndHandle.hide(); + mIsShowing = false; + if (mActionMode != null) { + // This will hide the mSelectionModifierCursorController + mActionMode.finish(); + } + } + + public boolean isActive() { + return mIsShowing; + } + + public void updatePosition(HandleView handle, int x, int y) { + final int scrollRows = mEmulator.getScreen().getActiveRows() - mEmulator.mRows; + if (y < mRenderer.mFontLineSpacing) {//up + mTopRow--; + if (mTopRow < -scrollRows) { + mTopRow = -scrollRows; + } + } else if (y + 2 * mRenderer.mFontLineSpacing > TerminalView.this.getHeight()) {//down + mTopRow++; + if (mTopRow > 0) { + mTopRow = 0; + } + } + if (handle == mStartHandle) { + mSelX1 = getCursorX(x); + mSelY1 = getCursorY(y); + if (mSelX1 < 0) { + mSelX1 = 0; + } + if (mSelY1 < -scrollRows) { + mSelY1 = -scrollRows; + } else if (mSelY1 > mEmulator.mRows - 1) { + mSelY1 = mEmulator.mRows - 1; + } + + if (mSelY1 > mSelY2) { + mSelY1 = mSelY2; + } + if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { + mSelX1 = mSelX2; + } + } else { + mSelX2 = getCursorX(x); + mSelY2 = getCursorY(y); + if (mSelX2 < 0) { + mSelX2 = 0; + } + if (mSelY2 < -scrollRows) { + mSelY2 = -scrollRows; + } else if (mSelY2 > mEmulator.mRows - 1) { + mSelY2 = mEmulator.mRows - 1; + } + + if (mSelY1 > mSelY2) { + mSelY2 = mSelY1; + } + if (mSelY1 == mSelY2 && mSelX1 > mSelX2) { + mSelX2 = mSelX1; + } + } invalidate(); - } else { - mActionMode.finish(); + } + + public void updatePosition() { + if (!isActive()) { + return; + } + + mStartHandle.positionAtCursor(mSelX1, mSelY1); + + mEndHandle.positionAtCursor(mSelX2 + 1, mSelY2); + + if (mActionMode != null) { + mActionMode.invalidate(); + } + + } + + public boolean onTouchEvent(MotionEvent event) { + + return false; + } + + + /** + * @return true iff this controller is currently used to move the selection start. + */ + public boolean isSelectionStartDragged() { + return mStartHandle.isDragging(); + } + + public boolean isSelectionEndDragged() { + return mEndHandle.isDragging(); + } + + public void onTouchModeChanged(boolean isInTouchMode) { + if (!isInTouchMode) { + hide(); + } + } + + @Override + public void onDetached() { + } + } + + SelectionModifierCursorController getSelectionController() { + if (mSelectionModifierCursorController == null) { + mSelectionModifierCursorController = new SelectionModifierCursorController(); + + final ViewTreeObserver observer = getViewTreeObserver(); + if (observer != null) { + observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); + } + } + + return mSelectionModifierCursorController; + } + + private void hideSelectionModifierCursorController() { + if (mSelectionModifierCursorController != null && mSelectionModifierCursorController.isActive()) { + mSelectionModifierCursorController.hide(); + } + } + + + private void startTextSelectionMode() { + if (!requestFocus()) { + return; + } + + getSelectionController().show(); + + mIsSelectingText = true; + + mClient.copyModeChanged(mIsSelectingText); + + invalidate(); + } + + private void stopTextSelectionMode() { + if (mIsSelectingText) { + hideSelectionModifierCursorController(); mSelX1 = mSelY1 = mSelX2 = mSelY2 = -1; + mIsSelectingText = false; + + mClient.copyModeChanged(mIsSelectingText); + invalidate(); } } - public TerminalSession getCurrentSession() { - return mTermSession; + + private final Runnable mShowFloatingToolbar = new Runnable() { + @Override + public void run() { + if (mActionMode != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mActionMode.hide(0); // hide off. + } + } + } + }; + + void hideFloatingToolbar(int duration) { + if (mActionMode != null) { + removeCallbacks(mShowFloatingToolbar); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + mActionMode.hide(duration); + } + } } - private CharSequence getText() { - return mEmulator.getScreen().getSelectedText(0, mTopRow, mEmulator.mColumns, mTopRow +mEmulator.mRows); + private void showFloatingToolbar() { + if (mActionMode != null) { + int delay = ViewConfiguration.getDoubleTapTimeout(); + postDelayed(mShowFloatingToolbar, delay); + } } + private void updateFloatingToolbarVisibility(MotionEvent event) { + if (mActionMode != null) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_MOVE: + hideFloatingToolbar(-1); + break; + case MotionEvent.ACTION_UP: // fall through + case MotionEvent.ACTION_CANCEL: + showFloatingToolbar(); + } + } + } } From 3b4ece6bd8970df992662c39845add4a1a9a60c4 Mon Sep 17 00:00:00 2001 From: mao Date: Sat, 5 Oct 2019 18:30:54 +0800 Subject: [PATCH 003/912] Selection mode fling --- terminal-view/src/main/java/com/termux/view/TerminalView.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 2065248779..09eae230cb 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -153,7 +153,7 @@ public boolean onScale(float focusX, float focusY, float scale) { @Override public boolean onFling(final MotionEvent e2, float velocityX, float velocityY) { - if (mEmulator == null || mIsSelectingText) return true; + if (mEmulator == null) return true; // Do not start scrolling until last fling has been taken care of: if (!mScroller.isFinished()) return true; From 937eb350b28766bd991c6111458f23bdff0a3c00 Mon Sep 17 00:00:00 2001 From: mao Date: Sat, 5 Oct 2019 18:54:15 +0800 Subject: [PATCH 004/912] Stop selection mode on enter --- terminal-view/src/main/java/com/termux/view/TerminalView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 09eae230cb..2cb3cb28c2 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -296,6 +296,7 @@ public boolean deleteSurroundingText(int leftLength, int rightLength) { } void sendTextToTerminal(CharSequence text) { + stopTextSelectionMode(); final int textLengthInChars = text.length(); for (int i = 0; i < textLengthInChars; i++) { char firstChar = text.charAt(i); @@ -542,6 +543,7 @@ public boolean onKeyDown(int keyCode, KeyEvent event) { if (LOG_KEY_EVENTS) Log.i(EmulatorDebug.LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (mEmulator == null) return true; + stopTextSelectionMode(); if (mClient.onKeyDown(keyCode, event, mTermSession)) { invalidate(); From fdb3764f5c11cc5a0581d5741938f22f8782f730 Mon Sep 17 00:00:00 2001 From: mao Date: Fri, 11 Oct 2019 07:37:57 +0800 Subject: [PATCH 005/912] Optimize handle view --- .../src/main/java/com/termux/view/TerminalView.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 2cb3cb28c2..e67a24df1f 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -10,6 +10,7 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.SystemClock; import android.text.Editable; import android.text.InputType; import android.text.TextUtils; @@ -882,6 +883,8 @@ private class HandleView extends View { public static final int RIGHT = 2; private int mHandleHeight; + private long mLastTime; + public HandleView(CursorController controller, int orientation) { super(TerminalView.this.getContext()); mController = controller; @@ -983,6 +986,11 @@ public boolean isShowing() { } private void checkChangedOrientation() { + long millis = SystemClock.currentThreadTimeMillis(); + if (millis - mLastTime < 50) { + return; + } + mLastTime = millis; final TerminalView hostView = TerminalView.this; final int left = hostView.getLeft(); @@ -1007,7 +1015,7 @@ private void checkChangedOrientation() { final int[] coords = mTempCoords; hostView.getLocationInWindow(coords); final int posX = coords[0] + mPointX; - if (posX + (int) mHotspotX < clip.left) { + if (posX < clip.left) { changeOrientation(RIGHT); } else if (posX + mHandleWidth > clip.right) { changeOrientation(LEFT); From 4189f598b937217cb70494ccda7b39a9d66426b6 Mon Sep 17 00:00:00 2001 From: Leonid Plyushch Date: Wed, 9 Oct 2019 23:00:51 +0300 Subject: [PATCH 006/912] add permission ACCESS_NETWORK_STATE Seems to be required by some Android TV devices. --- app/src/main/AndroidManifest.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17aecc5c71..8d45b96583 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + From c50a3670632c18c6107e2f25e5b9e7d61ff75c2a Mon Sep 17 00:00:00 2001 From: Fredrik Fornwall Date: Sun, 13 Oct 2019 20:48:09 +0200 Subject: [PATCH 007/912] Add .cxx folder to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a7327fbaaf..0b3a39edcb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ *.apk *.so .externalNativeBuild +.cxx # Crashlytics configuations com_crashlytics_export_strings.xml From 468f878a382d34cfe102f705c624fca23ab432d6 Mon Sep 17 00:00:00 2001 From: Leon Omelan Date: Sun, 6 Oct 2019 18:06:41 +0200 Subject: [PATCH 008/912] Unified UI colors across the app. Dark sidebar and dark app theme for dark Alert Dialogs --- app/src/main/java/com/termux/app/TermuxActivity.java | 2 +- app/src/main/res/drawable/current_session.xml | 4 ++-- app/src/main/res/drawable/session_ripple.xml | 4 ++-- app/src/main/res/layout/drawer_layout.xml | 2 +- app/src/main/res/values/styles.xml | 11 ++--------- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index 28f968ce0a..f3395716a8 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -454,7 +454,7 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { } else { firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } - int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED; + int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.WHITE : Color.RED; firstLineView.setTextColor(color); return row; } diff --git a/app/src/main/res/drawable/current_session.xml b/app/src/main/res/drawable/current_session.xml index e118aa0174..6a9264992f 100644 --- a/app/src/main/res/drawable/current_session.xml +++ b/app/src/main/res/drawable/current_session.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/app/src/main/res/drawable/session_ripple.xml b/app/src/main/res/drawable/session_ripple.xml index f38d75b66e..21423eb51b 100644 --- a/app/src/main/res/drawable/session_ripple.xml +++ b/app/src/main/res/drawable/session_ripple.xml @@ -2,6 +2,6 @@ - + - \ No newline at end of file + diff --git a/app/src/main/res/layout/drawer_layout.xml b/app/src/main/res/layout/drawer_layout.xml index c5117f14db..14ea1b646c 100644 --- a/app/src/main/res/layout/drawer_layout.xml +++ b/app/src/main/res/layout/drawer_layout.xml @@ -26,7 +26,7 @@ android:layout_width="240dp" android:layout_height="match_parent" android:layout_gravity="start" - android:background="@android:color/white" + android:background="@android:color/background_dark" android:choiceMode="singleChoice" android:divider="@android:color/transparent" android:dividerHeight="0dp" diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index f7c5cc3a0f..5dac43a178 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -3,14 +3,13 @@ - - - - From 5ba3f7cf6d8b2c29eb4f4d51a6a8961db502500d Mon Sep 17 00:00:00 2001 From: Leon Omelan Date: Mon, 7 Oct 2019 17:15:17 +0200 Subject: [PATCH 009/912] Made Black UI an option to configure --- .../java/com/termux/app/TermuxActivity.java | 28 ++++++++++++++++--- .../com/termux/app/TermuxPreferences.java | 7 +++++ app/src/main/res/drawable/current_session.xml | 2 +- .../res/drawable/current_session_black.xml | 4 +++ .../selected_session_background_black.xml | 5 ++++ app/src/main/res/drawable/session_ripple.xml | 2 +- .../res/drawable/session_ripple_black.xml | 7 +++++ app/src/main/res/layout/drawer_layout.xml | 2 +- app/src/main/res/values/styles.xml | 27 +++++++++++++++++- 9 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 app/src/main/res/drawable/current_session_black.xml create mode 100644 app/src/main/res/drawable/selected_session_background_black.xml create mode 100644 app/src/main/res/drawable/session_ripple_black.xml diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index f3395716a8..a84a7be7cb 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -127,6 +127,8 @@ public final class TermuxActivity extends Activity implements ServiceConnection */ boolean mIsVisible; + boolean mIsUsingBlackUI; + final SoundPool mBellSoundPool = new SoundPool.Builder().setMaxStreams(1).setAudioAttributes( new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION).build()).build(); @@ -203,11 +205,24 @@ public boolean ensureStoragePermissionGranted() { @Override public void onCreate(Bundle bundle) { - super.onCreate(bundle); - mSettings = new TermuxPreferences(this); + mIsUsingBlackUI = mSettings.isUsingBlackUI(); + if (mIsUsingBlackUI) { + this.setTheme(R.style.Theme_Termux_Black); + } else { + this.setTheme(R.style.Theme_Termux); + } + + super.onCreate(bundle); setContentView(R.layout.drawer_layout); + + if (mIsUsingBlackUI) { + findViewById(R.id.left_drawer).setBackgroundColor( + getResources().getColor(android.R.color.background_dark) + ); + } + mTerminalView = findViewById(R.id.terminal_view); mTerminalView.setOnKeyListener(new TermuxViewClient(this)); @@ -434,7 +449,11 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { boolean sessionRunning = sessionAtRow.isRunning(); TextView firstLineView = row.findViewById(R.id.row_line); - + if (mIsUsingBlackUI) { + firstLineView.setBackground( + getResources().getDrawable(R.drawable.selected_session_background_black) + ); + } String name = sessionAtRow.mSessionName; String sessionTitle = sessionAtRow.getTitle(); @@ -454,7 +473,8 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { } else { firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG); } - int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.WHITE : Color.RED; + int defaultColor = mIsUsingBlackUI ? Color.WHITE : Color.BLACK; + int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? defaultColor : Color.RED; firstLineView.setTextColor(color); return row; } diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java index 16a996ed20..f6095837fb 100644 --- a/app/src/main/java/com/termux/app/TermuxPreferences.java +++ b/app/src/main/java/com/termux/app/TermuxPreferences.java @@ -58,6 +58,7 @@ final static class KeyboardShortcut { private static final String CURRENT_SESSION_KEY = "current_session"; private static final String SCREEN_ALWAYS_ON_KEY = "screen_always_on"; + private String mUseDarkUI; private boolean mScreenAlwaysOn; private int mFontSize; @@ -126,6 +127,10 @@ boolean isScreenAlwaysOn() { return mScreenAlwaysOn; } + boolean isUsingBlackUI() { + return mUseDarkUI.toLowerCase().equals("true"); + } + void setScreenAlwaysOn(Context context, boolean newValue) { mScreenAlwaysOn = newValue; PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SCREEN_ALWAYS_ON_KEY, newValue).apply(); @@ -173,6 +178,8 @@ void reloadFromProperties(Context context) { break; } + mUseDarkUI = props.getProperty("use-black-ui", "false"); + try { JSONArray arr = new JSONArray(props.getProperty("extra-keys", "[['ESC', 'TAB', 'CTRL', 'ALT', '-', 'DOWN', 'UP']]")); diff --git a/app/src/main/res/drawable/current_session.xml b/app/src/main/res/drawable/current_session.xml index 6a9264992f..90dd28182b 100644 --- a/app/src/main/res/drawable/current_session.xml +++ b/app/src/main/res/drawable/current_session.xml @@ -1,4 +1,4 @@ - + diff --git a/app/src/main/res/drawable/current_session_black.xml b/app/src/main/res/drawable/current_session_black.xml new file mode 100644 index 0000000000..6a9264992f --- /dev/null +++ b/app/src/main/res/drawable/current_session_black.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/selected_session_background_black.xml b/app/src/main/res/drawable/selected_session_background_black.xml new file mode 100644 index 0000000000..25b7506f47 --- /dev/null +++ b/app/src/main/res/drawable/selected_session_background_black.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/session_ripple.xml b/app/src/main/res/drawable/session_ripple.xml index 21423eb51b..9c4a1e7954 100644 --- a/app/src/main/res/drawable/session_ripple.xml +++ b/app/src/main/res/drawable/session_ripple.xml @@ -2,6 +2,6 @@ - + diff --git a/app/src/main/res/drawable/session_ripple_black.xml b/app/src/main/res/drawable/session_ripple_black.xml new file mode 100644 index 0000000000..21423eb51b --- /dev/null +++ b/app/src/main/res/drawable/session_ripple_black.xml @@ -0,0 +1,7 @@ + + + + + + diff --git a/app/src/main/res/layout/drawer_layout.xml b/app/src/main/res/layout/drawer_layout.xml index 14ea1b646c..c5117f14db 100644 --- a/app/src/main/res/layout/drawer_layout.xml +++ b/app/src/main/res/layout/drawer_layout.xml @@ -26,7 +26,7 @@ android:layout_width="240dp" android:layout_height="match_parent" android:layout_gravity="start" - android:background="@android:color/background_dark" + android:background="@android:color/white" android:choiceMode="singleChoice" android:divider="@android:color/transparent" android:dividerHeight="0dp" diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5dac43a178..1f352423c9 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,9 +1,34 @@ + + + + - - @@ -46,4 +42,20 @@ true true + + + + + + + + + From 20d20f42c056ec72f4f486cd73c132b0b7d05b34 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:25:25 +0500 Subject: [PATCH 263/912] Added NotificationUtils to build and manage notifications The `TermuxPreferenceConstants` classes has been updated to `v0.5.0`. Check its Changelog sections for info on changes. --- .../TermuxAppSharedPreferences.java | 10 ++ .../TermuxPreferenceConstants.java | 19 +- .../termux/app/utils/NotificationUtils.java | 166 ++++++++++++++++++ 3 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 app/src/main/java/com/termux/app/utils/NotificationUtils.java diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxAppSharedPreferences.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxAppSharedPreferences.java index 33c12d9079..a1e663d7bc 100644 --- a/app/src/main/java/com/termux/app/settings/preferences/TermuxAppSharedPreferences.java +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxAppSharedPreferences.java @@ -122,6 +122,16 @@ public void setLogLevel(Context context, int logLevel) { + public int getLastNotificationId() { + return SharedPreferenceUtils.getInt(mSharedPreferences, TERMUX_APP.KEY_LAST_NOTIFICATION_ID, TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID); + } + + public void setLastNotificationId(int notificationId) { + SharedPreferenceUtils.setInt(mSharedPreferences, TERMUX_APP.KEY_LAST_NOTIFICATION_ID, notificationId, false); + } + + + public boolean getTerminalViewKeyLoggingEnabled() { return SharedPreferenceUtils.getBoolean(mSharedPreferences, TERMUX_APP.KEY_TERMINAL_VIEW_KEY_LOGGING_ENABLED, TERMUX_APP.DEFAULT_VALUE_TERMINAL_VIEW_KEY_LOGGING_ENABLED); } diff --git a/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java index 605e9f0610..51ac344f84 100644 --- a/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java +++ b/app/src/main/java/com/termux/app/settings/preferences/TermuxPreferenceConstants.java @@ -1,7 +1,7 @@ package com.termux.app.settings.preferences; /* - * Version: v0.4.0 + * Version: v0.5.0 * * Changelog * @@ -9,19 +9,23 @@ * - Initial Release. * * - 0.2.0 (2021-03-13) - * - Added `KEY_LOG_LEVEL` and `KEY_TERMINAL_VIEW_LOGGING_ENABLED` - * + * - 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`. + * `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`. */ /** @@ -70,6 +74,13 @@ public static final class TERMUX_APP { 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 */ diff --git a/app/src/main/java/com/termux/app/utils/NotificationUtils.java b/app/src/main/java/com/termux/app/utils/NotificationUtils.java new file mode 100644 index 0000000000..9c83132ed1 --- /dev/null +++ b/app/src/main/java/com/termux/app/utils/NotificationUtils.java @@ -0,0 +1,166 @@ +package com.termux.app.utils; + +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.app.RunCommandService; +import com.termux.app.TermuxService; +import com.termux.app.settings.preferences.TermuxAppSharedPreferences; +import com.termux.app.settings.preferences.TermuxPreferenceConstants; + +public class NotificationUtils { + + /** Do not show notification */ + public static final int NOTIFICATION_MODE_NONE = 0; + /** Show notification without sound, vibration or lights */ + public static final int NOTIFICATION_MODE_SILENT = 1; + /** Show notification with sound */ + public static final int NOTIFICATION_MODE_SOUND = 2; + /** Show notification with vibration */ + public static final int NOTIFICATION_MODE_VIBRATE = 3; + /** Show notification with lights */ + public static final int NOTIFICATION_MODE_LIGHTS = 4; + /** Show notification with sound and vibration */ + public static final int NOTIFICATION_MODE_SOUND_AND_VIBRATE = 5; + /** Show notification with sound and lights */ + public static final int NOTIFICATION_MODE_SOUND_AND_LIGHTS = 6; + /** Show notification with vibration and lights */ + public static final int NOTIFICATION_MODE_VIBRATE_AND_LIGHTS = 7; + /** Show notification with sound, vibration and lights */ + public static final int NOTIFICATION_MODE_ALL = 8; + + private static final String LOG_TAG = "NotificationUtils"; + + /** + * Get the {@link NotificationManager}. + * + * @param context The {@link Context} for operations. + * @return Returns the {@link NotificationManager}. + */ + @Nullable + public static NotificationManager getNotificationManager(final Context context) { + if(context == null) return null; + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Try to get the next unique notification id that isn't already being used by the app. + * + * @param context The {@link Context} for operations. + * @return Returns the notification id that should be safe to use. + */ + public synchronized static int getNextNotificationId(final Context context) { + if(context == null) return TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID; + + TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context); + int lastNotificationId = preferences.getLastNotificationId(); + + int nextNotificationId = lastNotificationId + 1; + while(nextNotificationId == TermuxService.NOTIFICATION_ID || nextNotificationId == RunCommandService.NOTIFICATION_ID) { + nextNotificationId++; + } + + if(nextNotificationId == Integer.MAX_VALUE || nextNotificationId < 0) + nextNotificationId = TermuxPreferenceConstants.TERMUX_APP.DEFAULT_VALUE_KEY_LAST_NOTIFICATION_ID; + + preferences.setLastNotificationId(nextNotificationId); + return nextNotificationId; + } + + /** + * Get {@link Notification.Builder}. + * + * @param context The {@link Context} for operations. + * @param title The title for the notification. + * @param notifiationText The second line text of the notification. + * @param notificationBigText The full text of the notification that may optionally be styled. + * @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked. + * @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}. + * The builder returned will be {@code null} if {@link #NOTIFICATION_MODE_NONE} + * is passed. That case should ideally be handled before calling this function. + * @return Returns the {@link Notification.Builder}. + */ + @Nullable + public static Notification.Builder geNotificationBuilder(final Context context, final String channelId, final int priority, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) { + if(context == null) return null; + Notification.Builder builder = new Notification.Builder(context); + builder.setContentTitle(title); + builder.setContentText(notifiationText); + builder.setStyle(new Notification.BigTextStyle().bigText(notificationBigText)); + builder.setContentIntent(pendingIntent); + + builder.setPriority(priority); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + builder.setChannelId(channelId); + + builder = setNotificationDefaults(builder, notificationMode); + + return builder; + } + + /** + * Setup the notification channel if Android version is greater than or equal to + * {@link Build.VERSION_CODES#O}. + * + * @param context The {@link Context} for operations. + * @param channelId The id of the channel. Must be unique per package. + * @param channelName The user visible name of the channel. + * @param importance The importance of the channel. This controls how interruptive notifications + * posted to this channel are. + */ + public static void setupNotificationChannel(final Context context, final String channelId, final CharSequence channelName, final int importance) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + NotificationChannel channel = new NotificationChannel(channelId, channelName, importance); + + NotificationManager notificationManager = getNotificationManager(context); + if(notificationManager != null) + notificationManager.createNotificationChannel(channel); + } + + public static Notification.Builder setNotificationDefaults(Notification.Builder builder, final int notificationMode) { + + // TODO: setDefaults() is deprecated and should also implement setting notification mode via notification channel + switch (notificationMode) { + case NOTIFICATION_MODE_NONE: + Logger.logWarn(LOG_TAG, "The NOTIFICATION_MODE_NONE passed to setNotificationDefaults(), force setting builder to null."); + return null; // return null since notification is not supposed to be shown + case NOTIFICATION_MODE_SILENT: + break; + case NOTIFICATION_MODE_SOUND: + builder.setDefaults(Notification.DEFAULT_SOUND); + break; + case NOTIFICATION_MODE_VIBRATE: + builder.setDefaults(Notification.DEFAULT_VIBRATE); + break; + case NOTIFICATION_MODE_LIGHTS: + builder.setDefaults(Notification.DEFAULT_LIGHTS); + break; + case NOTIFICATION_MODE_SOUND_AND_VIBRATE: + builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_VIBRATE); + break; + case NOTIFICATION_MODE_SOUND_AND_LIGHTS: + builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS); + break; + case NOTIFICATION_MODE_VIBRATE_AND_LIGHTS: + builder.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_LIGHTS); + break; + case NOTIFICATION_MODE_ALL: + builder.setDefaults(Notification.DEFAULT_ALL); + break; + default: + Logger.logError(LOG_TAG, "Invalid notificationMode: \"" + notificationMode + "\" passed to setNotificationDefaults()"); + break; + } + + return builder; + } + +} From bccc35bc3fb19c25580c384d3dfd5f1a7d3daaa2 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:48:24 +0500 Subject: [PATCH 264/912] Added ExectionCommand ExectionCommand is a class that stores all data related to an execution command like: - Input parameters like executable and arguments to be used to run the shell command, etc - Output parameters like stdout, stderr and exitCode. - Error info generated internally by termux outside the shell in errCode and errmsg. - Command info like, id, label, description, help info, etc. - Other config info like for how termux should handle the command. - The pending intent if any that should be sent after execution to command requester. - The help for the plugin API that was used to send the intent. - Current and previous state of the command. This allow easier management and passing of execution command data between classes and management of it. This will later allow each ExectionCommand command to be linked to a Terminal Session, to handle post processing and failure management. The ExectionCommand also provides functions to get its data in markdown format, which can be used by failure or success reports generated for the command that are shown to the user. The commandHelp and pluginAPIHelp can also be specially useful to provide info to users on how to manage failures that are generated. --- .../com/termux/models/ExecutionCommand.java | 531 ++++++++++++++++++ 1 file changed, 531 insertions(+) create mode 100644 app/src/main/java/com/termux/models/ExecutionCommand.java diff --git a/app/src/main/java/com/termux/models/ExecutionCommand.java b/app/src/main/java/com/termux/models/ExecutionCommand.java new file mode 100644 index 0000000000..fa94498c71 --- /dev/null +++ b/app/src/main/java/com/termux/models/ExecutionCommand.java @@ -0,0 +1,531 @@ +package com.termux.models; + +import android.app.PendingIntent; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import com.termux.app.utils.Logger; +import com.termux.app.utils.MarkdownUtils; +import com.termux.app.utils.TextDataUtils; + +import java.util.ArrayList; +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}. */ + public ExecutionState currentState = ExecutionState.PRE_EXECUTION; + /** The previous state of the {@link ExecutionCommand}. */ + public 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 current working directory for the {@link ExecutionCommand}. */ + public String workingDirectory; + + + /** 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 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 if {@link ExecutionCommand} was started because of an external plugin request or from + * within Termux app itself. */ + public boolean isPluginExecutionCommand; + /** Defines {@link PendingIntent} that should be sent if an external plugin requested the execution. */ + public PendingIntent pluginPendingIntent; + + + /** The stdout of shell command. */ + public String stdout; + /** The sterr of shell command. */ + public String stderr; + /** The exit code of shell command. */ + public Integer exitCode; + + + /** The internal error code of {@link ExecutionCommand}. */ + public Integer errCode; + /** The internal error message of {@link ExecutionCommand}. */ + public String errmsg; + /** The internal exceptions of {@link ExecutionCommand}. */ + public List throwableList = new ArrayList<>(); + + + + public ExecutionCommand(){ + } + + public ExecutionCommand(Integer id){ + this.id = id; + } + + public ExecutionCommand(Integer id, String executable, String[] arguments, String workingDirectory, boolean inBackground, boolean isFailsafe) { + this.id = id; + this.executable = executable; + this.arguments = arguments; + this.workingDirectory = workingDirectory; + this.inBackground = inBackground; + this.isFailsafe = isFailsafe; + } + + @NonNull + @Override + public String toString() { + if(currentState.getValue() < ExecutionState.EXECUTED.getValue()) + return getExecutionInputLogString(this, true); + else { + return getExecutionOutputLogString(this, 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. + * @return Returns the log friendly {@link String}. + */ + public static String getExecutionInputLogString(ExecutionCommand executionCommand, boolean ignoreNull) { + if (executionCommand == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append(executionCommand.getIdLogString()); + logString.append(executionCommand.getCommandLabelLogString()).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(!ignoreNull || executionCommand.sessionAction != null) + logString.append("\n").append(executionCommand.getSessionActionLogString()); + + logString.append("\n").append(executionCommand.getIsPluginExecutionCommandLogString()); + if(!ignoreNull || executionCommand.isPluginExecutionCommand) { + if (!ignoreNull || executionCommand.pluginPendingIntent != null) + logString.append("\n").append(executionCommand.getPendingIntentCreatorLogString()); + } + + 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. + * @return Returns the log friendly {@link String}. + */ + public static String getExecutionOutputLogString(ExecutionCommand executionCommand, boolean ignoreNull) { + if (executionCommand == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append(executionCommand.getIdLogString()); + logString.append(executionCommand.getCommandLabelLogString()).append(":"); + + logString.append("\n").append(executionCommand.getPreviousStateLogString()); + logString.append("\n").append(executionCommand.getCurrentStateLogString()); + + logString.append("\n").append(executionCommand.getStdoutLogString()); + logString.append("\n").append(executionCommand.getStderrLogString()); + logString.append("\n").append(executionCommand.getExitCodeLogString()); + + logString.append(getExecutionErrLogString(executionCommand, ignoreNull)); + + return logString.toString(); + } + + /** + * Get a log friendly {@link String} for {@link ExecutionCommand} execution error parameters. + * + * @param executionCommand The {@link ExecutionCommand} 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 getExecutionErrLogString(ExecutionCommand executionCommand, boolean ignoreNull) { + StringBuilder logString = new StringBuilder(); + + if(!ignoreNull || (executionCommand.errCode != null && executionCommand.errCode != 0)) { + logString.append("\n").append(executionCommand.getErrCodeLogString()); + logString.append("\n").append(executionCommand.getErrmsgLogString()); + logString.append("\n").append(executionCommand.geStackTracesLogString()); + } else { + logString.append(""); + } + + 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(ExecutionCommand executionCommand) { + if (executionCommand == null) return "null"; + + StringBuilder logString = new StringBuilder(); + + logString.append(getExecutionInputLogString(executionCommand, false)); + logString.append(getExecutionOutputLogString(executionCommand, false)); + + 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 getDetailedMarkdownString(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, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Session Action", executionCommand.sessionAction, "-")); + + + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("isPluginExecutionCommand", executionCommand.isPluginExecutionCommand, "-")); + if (executionCommand.pluginPendingIntent != null) + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Pending Intent Creator", executionCommand.pluginPendingIntent.getCreatorPackage(), "-")); + else + markdownString.append("\n").append("**Pending Intent Creator:** - "); + + markdownString.append("\n\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stdout", executionCommand.stdout, "-")); + markdownString.append("\n").append(MarkdownUtils.getMultiLineMarkdownStringEntry("Stderr", executionCommand.stderr, "-")); + markdownString.append("\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Exit Code", executionCommand.exitCode, "-")); + + markdownString.append("\n\n").append(MarkdownUtils.getSingleLineMarkdownStringEntry("Err Code", executionCommand.errCode, "-")); + markdownString.append("\n").append("**Errmsg:**\n").append(TextDataUtils.getDefaultIfNull(executionCommand.errmsg, "-")); + markdownString.append("\n\n").append(executionCommand.geStackTracesMarkdownString()); + + 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 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 getIsPluginExecutionCommandLogString() { + return "isPluginExecutionCommand: `" + isPluginExecutionCommand + "`"; + } + + public String getSessionActionLogString() { + return getSingleLineLogStringEntry("Session Action", sessionAction, "-"); + } + + public String getPendingIntentCreatorLogString() { + if (pluginPendingIntent != null) + return "Pending Intent Creator: `" + pluginPendingIntent.getCreatorPackage() + "`"; + else + return "Pending Intent Creator: -"; + } + + public String getCommandDescriptionLogString() { + return getSingleLineLogStringEntry("Command Description", commandDescription, "-"); + } + + public String getCommandHelpLogString() { + return getSingleLineLogStringEntry("Command Help", commandHelp, "-"); + } + + public String getPluginAPIHelpLogString() { + return getSingleLineLogStringEntry("Plugin API Help", pluginAPIHelp, "-"); + } + + public String getStdoutLogString() { + return getMultiLineLogStringEntry("Stdout", stdout, "-"); + } + + public String getStderrLogString() { + return getMultiLineLogStringEntry("Stderr", stderr, "-"); + } + + public String getExitCodeLogString() { + return getSingleLineLogStringEntry("Exit Code", exitCode, "-"); + } + + public String getErrCodeLogString() { + return getSingleLineLogStringEntry("Err Code", errCode, "-"); + } + + public String getErrmsgLogString() { + return getMultiLineLogStringEntry("Errmsg", errmsg, "-"); + } + + public String geStackTracesLogString() { + return Logger.getStackTracesString("StackTraces:", Logger.getStackTraceStringArray(throwableList)); + } + + public String geStackTracesMarkdownString() { + return Logger.getStackTracesMarkdownString("StackTraces:", Logger.getStackTraceStringArray(throwableList)); + } + + + + /** + * 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(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(); + } + + + /** + * Get a log friendly {@link String} for {@link List} 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(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(getSingleLineLogStringEntry("Arg " + (i + 1), argumentsArray[i], "-")).append("`\n"); + } + argumentsString.append("```"); + } else{ + argumentsString.append(" -"); + } + + return argumentsString.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 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("Invalid ExecutionCommand 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 boolean setStateFailed(int errCode, String errmsg, Throwable throwable) { + if(errCode < 1) + return false; + + if(!setState(ExecutionState.FAILED)) + return false; + + this.errCode = errCode; + this.errmsg = errmsg; + + if(this.throwableList == null) + this.throwableList = new ArrayList<>(); + + if(throwable != null) + this.throwableList.add(throwable); + + return true; + } + +} From ef1ab197b6481c9ffb3a29bf0452b7b5d7f87cdf Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:51:57 +0500 Subject: [PATCH 265/912] Update TermuxConstants The `TermuxConstants` classes has been updated to `v0.11.0`. Check its Changelog sections for info on changes. --- .../java/com/termux/app/TermuxConstants.java | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxConstants.java b/app/src/main/java/com/termux/app/TermuxConstants.java index c5497d45dc..f578509002 100644 --- a/app/src/main/java/com/termux/app/TermuxConstants.java +++ b/app/src/main/java/com/termux/app/TermuxConstants.java @@ -5,7 +5,7 @@ import java.io.File; /* - * Version: v0.10.0 + * Version: v0.11.0 * * Changelog * @@ -55,8 +55,7 @@ * - Added following to `TERMUX_SERVICE`: * `EXTRA_PENDING_INTENT`, `EXTRA_RESULT_BUNDLE`, * `EXTRA_STDOUT`, `EXTRA_STDERR`, `EXTRA_EXIT_CODE`, - * `EXTRA_ERR`, `EXTRA_ERRMSG` - * . + * `EXTRA_ERR`, `EXTRA_ERRMSG`. * * - 0.9.0 (2021-03-18) * - Fixed javadocs. @@ -67,9 +66,16 @@ * `VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY`, * `VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_OPEN_ACTIVITY`, * `VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_DONT_OPEN_ACTIVITY` - * `VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY` + * `VALUE_EXTRA_SESSION_ACTION_KEEP_CURRENT_SESSION_AND_DONT_OPEN_ACTIVITY`. * - Added following to `RUN_COMMAND_SERVICE`: - * `EXTRA_SESSION_ACTION` + * `EXTRA_SESSION_ACTION`. + * + * - 0.11.0 (2021-03-24) + * - Added following to `TERMUX_SERVICE`: + * `EXTRA_COMMAND_LABEL`, `EXTRA_COMMAND_DESCRIPTION`, `EXTRA_COMMAND_HELP`, `EXTRA_PLUGIN_API_HELP`. + * - Added following to `RUN_COMMAND_SERVICE`: + * `EXTRA_COMMAND_LABEL`, `EXTRA_COMMAND_DESCRIPTION`, `EXTRA_COMMAND_HELP`. + * - Updated `RESULT_BUNDLE` related extras with `PLUGIN_RESULT_BUNDLE` prefixes. */ /** @@ -432,6 +438,14 @@ public static final class TERMUX_SERVICE { public static final String EXTRA_SESSION_ACTION = TERMUX_PACKAGE_NAME + ".execute.session_action"; // Default: "com.termux.execute.session_action" /** Intent {@code Parcelable} extra containing pending intent for the execute command caller */ public static final String EXTRA_PENDING_INTENT = "pendingIntent"; // Default: "pendingIntent" + /** Intent {@code String} extra for label of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_COMMAND_LABEL = TERMUX_PACKAGE_NAME + ".execute.command_label"; // Default: "com.termux.execute.command_label" + /** Intent markdown {@code String} extra for description of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".execute.command_description"; // Default: "com.termux.execute.command_description" + /** Intent markdown {@code String} extra for help of the command for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent */ + public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".execute.command_help"; // Default: "com.termux.execute.command_help" + /** Intent markdown {@code String} extra for help of the plugin API for the TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent (Internal Use Only) */ + public static final String EXTRA_PLUGIN_API_HELP = TERMUX_PACKAGE_NAME + ".execute.plugin_api_help"; // Default: "com.termux.execute.plugin_help" @@ -469,17 +483,17 @@ public static final class TERMUX_SERVICE { /** Intent {@code Bundle} extra to store result of execute command that is sent back for the * TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent if the {@link #EXTRA_PENDING_INTENT} is not * {@code null} */ - public static final String EXTRA_RESULT_BUNDLE = "result"; // Default: "result" - /** Intent {@code String} extra for stdout value of execute command of the {@link #EXTRA_RESULT_BUNDLE} */ - public static final String EXTRA_STDOUT = "stdout"; // Default: "stdout" - /** Intent {@code String} extra for stderr value of execute command of the {@link #EXTRA_RESULT_BUNDLE} */ - public static final String EXTRA_STDERR = "stderr"; // Default: "stderr" - /** Intent {@code int} extra for exit code value of execute command of the {@link #EXTRA_RESULT_BUNDLE} */ - public static final String EXTRA_EXIT_CODE = "exitCode"; // Default: "exitCode" - /** Intent {@code int} extra for err value of execute command of the {@link #EXTRA_RESULT_BUNDLE} */ - public static final String EXTRA_ERR = "err"; // Default: "err" - /** Intent {@code String} extra for errmsg value of execute command of the {@link #EXTRA_RESULT_BUNDLE} */ - public static final String EXTRA_ERRMSG = "errmsg"; // Default: "errmsg" + public static final String EXTRA_PLUGIN_RESULT_BUNDLE = "result"; // Default: "result" + /** Intent {@code String} extra for stdout value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */ + public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDOUT = "stdout"; // Default: "stdout" + /** Intent {@code String} extra for stderr value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */ + public static final String EXTRA_PLUGIN_RESULT_BUNDLE_STDERR = "stderr"; // Default: "stderr" + /** Intent {@code int} extra for exit code value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */ + public static final String EXTRA_PLUGIN_RESULT_BUNDLE_EXIT_CODE = "exitCode"; // Default: "exitCode" + /** Intent {@code int} extra for err value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */ + public static final String EXTRA_PLUGIN_RESULT_BUNDLE_ERR = "err"; // Default: "err" + /** Intent {@code String} extra for errmsg value of execute command of the {@link #EXTRA_PLUGIN_RESULT_BUNDLE} */ + public static final String EXTRA_PLUGIN_RESULT_BUNDLE_ERRMSG = "errmsg"; // Default: "errmsg" } @@ -507,6 +521,12 @@ public static final class RUN_COMMAND_SERVICE { public static final String EXTRA_BACKGROUND = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_BACKGROUND"; // Default: "com.termux.RUN_COMMAND_BACKGROUND" /** Intent {@code String} extra for session action of foreground commands for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ public static final String EXTRA_SESSION_ACTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_SESSION_ACTION"; // Default: "com.termux.RUN_COMMAND_SESSION_ACTION" + /** Intent {@code String} extra for label of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_COMMAND_LABEL = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_LABEL"; // Default: "com.termux.RUN_COMMAND_COMMAND_LABEL" + /** Intent markdown {@code String} extra for description of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_COMMAND_DESCRIPTION = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_DESCRIPTION"; // Default: "com.termux.RUN_COMMAND_COMMAND_DESCRIPTION" + /** Intent markdown {@code String} extra for help of the command for the RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND intent */ + public static final String EXTRA_COMMAND_HELP = TERMUX_PACKAGE_NAME + ".RUN_COMMAND_COMMAND_HELP"; // Default: "com.termux.RUN_COMMAND_COMMAND_HELP" } } From 31371b5e3df946fe1b5524e24aadd826b853d6b4 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:56:25 +0500 Subject: [PATCH 266/912] Fully integrate ExectionCommand into RunCommandService Users will now also be shown flashes and notifications in addition to log entries for missing allow-external-apps permission or for invalid extras passed like the executable. The flashes and notifications can be controlled with the Termux Settings -> Debugging -> Plugin Error Notifications toggle --- .../com/termux/app/RunCommandService.java | 116 ++++++---- .../com/termux/app/utils/PluginUtils.java | 210 ++++++++++-------- .../com/termux/app/utils/TextDataUtils.java | 13 ++ .../res/drawable/ic_error_notification.xml | 37 +++ app/src/main/res/values/strings.xml | 10 +- 5 files changed, 250 insertions(+), 136 deletions(-) create mode 100644 app/src/main/res/drawable/ic_error_notification.xml diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index a5236e7c4e..f8abb0a986 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -10,7 +10,6 @@ import android.os.Binder; import android.os.Build; import android.os.IBinder; -import android.util.Log; import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.RUN_COMMAND_SERVICE; @@ -18,9 +17,9 @@ import com.termux.app.utils.FileUtils; import com.termux.app.utils.Logger; import com.termux.app.utils.PluginUtils; - -import java.util.Arrays; -import java.util.HashMap; +import com.termux.app.utils.TextDataUtils; +import com.termux.models.ExecutionCommand; +import com.termux.models.ExecutionCommand.ExecutionState; /** * Third-party apps that are not part of termux world can run commands in termux context by either @@ -88,18 +87,24 @@ * * The {@link RUN_COMMAND_SERVICE#ACTION_RUN_COMMAND} intent expects the following extras: * - * 1. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for absolute path of - * command. This is mandatory. + * 1. The **mandatory** {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} extra for + * absolute path of command. * 2. The {@code String[]} {@link RUN_COMMAND_SERVICE#EXTRA_ARGUMENTS} extra for any arguments to - * pass to command. This is optional. + * pass to command. * 3. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} extra for current working directory - * of command. This is optional and defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}. + * of command. This defaults to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}. * 4. The {@code boolean} {@link RUN_COMMAND_SERVICE#EXTRA_BACKGROUND} extra whether to run command - * in background or foreground terminal session. This is optional and defaults to {@code false}. + * in background or foreground terminal session. This defaults to {@code false}. * 5. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_SESSION_ACTION} extra for for session action - * of foreground commands. This is optional and defaults to + * of foreground commands. This defaults to * {@link TERMUX_SERVICE#VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY}. - * + * 6. The {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_LABEL} extra for label of the command. + * 7. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_DESCRIPTION} extra for + * description of the command. This should ideally be get short. + * 8. The markdown {@code String} {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_HELP} extra for help of + * the command. This can add details about the command. 3rd party apps can provide more info + * to users for setting up commands. Ideally a url link should be provided that goes into full + * details. * * * The {@link RUN_COMMAND_SERVICE#EXTRA_COMMAND_PATH} and {@link RUN_COMMAND_SERVICE#EXTRA_WORKDIR} @@ -107,6 +112,20 @@ * The "$PREFIX/" will expand to {@link TermuxConstants#TERMUX_PREFIX_DIR_PATH} and * "~/" will expand to {@link TermuxConstants#TERMUX_HOME_DIR_PATH}, followed by a forward slash "/". * + * + * The `EXTRA_COMMAND_*` extras are used for logging and are their values are provided to users in case + * of failure in a popup. The popup shown is in commonmark-spec markdown using markwon library so + * make sure to follow its formatting rules. Also make sure to end lines with 2 blank spaces to prevent + * word-wrap wherever needed. + * It's the users and 3rd party apps responsibility to use them wisely. There are also android + * internal intent size limits (roughly 500KB) that must not exceed when sending intents so make sure + * the combined size of ALL extras is less than that. + * There are also limits on the arguments size you can pass to commands or the full command string + * length that can be run, which is likely equal to 131072 bytes or 128KB on an android device. + * Check https://github.com/termux/termux-tasker#arguments-and-result-data-limits for more details. + * + * + * * 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` @@ -138,7 +157,7 @@ public class RunCommandService extends Service { private static final String NOTIFICATION_CHANNEL_ID = "termux_run_command_notification_channel"; - private static final int NOTIFICATION_ID = 1338; + public static final int NOTIFICATION_ID = 1338; private static final String LOG_TAG = "RunCommandService"; @@ -166,78 +185,91 @@ public int onStartCommand(Intent intent, int flags, int startId) { // Run again in case service is already started and onCreate() is not called runStartForeground(); + ExecutionCommand executionCommand = new ExecutionCommand(); + executionCommand.pluginAPIHelp = this.getString(R.string.run_command_service_api_help); + String errmsg; // If invalid action passed, then just return if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction()); - Logger.logError(LOG_TAG, errmsg); + executionCommand.setStateFailed(1, errmsg, null); + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; } + executionCommand.executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); + executionCommand.arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS); + executionCommand.workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); + executionCommand.inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); + executionCommand.sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); + executionCommand.commandLabel = TextDataUtils.getDefaultIfNull(intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_LABEL), "RUN_COMMAND Execution Intent Command"); + executionCommand.commandDescription = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_DESCRIPTION); + executionCommand.commandHelp = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_HELP); + + if(!executionCommand.setState(ExecutionState.PRE_EXECUTION)) + return Service.START_NOT_STICKY; + // If "allow-external-apps" property to not set to "true", then just return errmsg = PluginUtils.checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(this); if (errmsg != null) { - Logger.logError(LOG_TAG, errmsg); + executionCommand.setStateFailed(1, errmsg, null); + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; } - - String executable = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_COMMAND_PATH); - String[] arguments = intent.getStringArrayExtra(RUN_COMMAND_SERVICE.EXTRA_ARGUMENTS); - boolean inBackground = intent.getBooleanExtra(RUN_COMMAND_SERVICE.EXTRA_BACKGROUND, false); - String workingDirectory = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_WORKDIR); - String sessionAction = intent.getStringExtra(RUN_COMMAND_SERVICE.EXTRA_SESSION_ACTION); - // Get canonical path of executable - executable = FileUtils.getCanonicalPath(executable, null, true); + executionCommand.executable = FileUtils.getCanonicalPath(executionCommand.executable, null, true); // If executable is not a regular file, or is not readable or executable, then just return // Setting of missing read and execute permissions is not done - errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executable, + errmsg = FileUtils.validateRegularFileExistenceAndPermissions(this, executionCommand.executable, null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, false, false); if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.executable_absolute_path, executable); - Logger.logError(LOG_TAG, errmsg); + errmsg += "\n" + this.getString(R.string.executable_absolute_path, executionCommand.executable); + executionCommand.setStateFailed(1, errmsg, null); + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; } // If workingDirectory is not null or empty - if (workingDirectory != null && !workingDirectory.isEmpty()) { + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) { // Get canonical path of workingDirectory - workingDirectory = FileUtils.getCanonicalPath(workingDirectory, null, true); + executionCommand.workingDirectory = FileUtils.getCanonicalPath(executionCommand.workingDirectory, null, true); // If workingDirectory 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 workingDirectory is // under {@link TermuxConstants#TERMUX_FILES_DIR_PATH} // We try to set execute permissions, but ignore if they are missing, since only read and write permissions are required // for working directories. - errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, workingDirectory, + errmsg = FileUtils.validateDirectoryExistenceAndPermissions(this, executionCommand.workingDirectory, TermuxConstants.TERMUX_FILES_DIR_PATH, PluginUtils.PLUGIN_WORKING_DIRECTORY_PERMISSIONS, true, true, false, true); if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, workingDirectory); - Logger.logError(LOG_TAG, errmsg); + errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, executionCommand.workingDirectory); + executionCommand.setStateFailed(1, errmsg, null); + PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; } } - PluginUtils.dumpExecutionIntentToLog(Log.VERBOSE, LOG_TAG, "RUN_COMMAND Intent", executable, Arrays.asList(arguments), workingDirectory, inBackground, new HashMap() {{ - put("sessionAction", sessionAction); - }}); + executionCommand.executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executionCommand.executable)).build(); - Uri executableUri = new Uri.Builder().scheme(TERMUX_SERVICE.URI_SCHEME_SERVICE_EXECUTE).path(FileUtils.getExpandedTermuxPath(executable)).build(); + Logger.logVerbose(LOG_TAG, executionCommand.toString()); // Create execution intent with the action TERMUX_SERVICE#ACTION_SERVICE_EXECUTE to be sent to the TERMUX_SERVICE - Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executableUri); + Intent execIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE, executionCommand.executableUri); execIntent.setClass(this, TermuxService.class); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, arguments); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, inBackground); - if (workingDirectory != null && !workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, workingDirectory); - execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, sessionAction); - + execIntent.putExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS, executionCommand.arguments); + if (executionCommand.workingDirectory != null && !executionCommand.workingDirectory.isEmpty()) execIntent.putExtra(TERMUX_SERVICE.EXTRA_WORKDIR, executionCommand.workingDirectory); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, executionCommand.inBackground); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION, executionCommand.sessionAction); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL, executionCommand.commandLabel); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION, executionCommand.commandDescription); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP, executionCommand.commandHelp); + execIntent.putExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP, executionCommand.pluginAPIHelp); // Start TERMUX_SERVICE and pass it execution intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -292,8 +324,8 @@ private void setupNotificationChannel() { int importance = NotificationManager.IMPORTANCE_LOW; NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); } } diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/app/src/main/java/com/termux/app/utils/PluginUtils.java index e59af7f830..0fd2a7e9c1 100644 --- a/app/src/main/java/com/termux/app/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/app/utils/PluginUtils.java @@ -1,38 +1,31 @@ package com.termux.app.utils; import android.app.Activity; +import android.app.Notification; +import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import androidx.annotation.Nullable; + import com.termux.R; import com.termux.app.TermuxConstants; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_SERVICE; +import com.termux.app.activities.ReportActivity; +import com.termux.app.settings.preferences.TermuxAppSharedPreferences; +import com.termux.app.settings.preferences.TermuxPreferenceConstants; import com.termux.app.settings.properties.SharedProperties; import com.termux.app.settings.properties.TermuxPropertyConstants; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import com.termux.models.ReportInfo; +import com.termux.models.ExecutionCommand; +import com.termux.models.UserAction; public class PluginUtils { - /** Plugin variable for stdout value of termux command */ - public static final String PLUGIN_VARIABLE_STDOUT = "%stdout"; // Default: "%stdout" - /** Plugin variable for stderr value of termux command */ - public static final String PLUGIN_VARIABLE_STDERR = "%stderr"; // Default: "%stderr" - /** Plugin variable for exit code value of termux command */ - public static final String PLUGIN_VARIABLE_EXIT_CODE = "%result"; // Default: "%result" - /** Plugin variable for err value of termux command */ - public static final String PLUGIN_VARIABLE_ERR = "%err"; // Default: "%err" - /** Plugin variable for errmsg value of termux command */ - public static final String PLUGIN_VARIABLE_ERRMSG = "%errmsg"; // Default: "%errmsg" - - /** Intent {@code Parcelable} extra containing original intent received from plugin host app by FireReceiver */ - public static final String EXTRA_ORIGINAL_INTENT = "originalIntent"; // Default: "originalIntent" - - + private static final String NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS = "termux_plugin_command_errors_notification_channel"; + private static final String NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS = TermuxConstants.TERMUX_APP_NAME + " Plugin Commands Errors"; /** Required file permissions for the executable file of execute intent. Executable file must have read and execute permissions */ public static final String PLUGIN_EXECUTABLE_FILE_PERMISSIONS = "r-x"; // Default: "r-x" @@ -40,34 +33,23 @@ public class PluginUtils { * Execute permissions should be attempted to be set, but ignored if they are missing */ public static final String PLUGIN_WORKING_DIRECTORY_PERMISSIONS = "rwx"; // Default: "rwx" - - - /** - * A regex to validate if a string matches a valid plugin host variable name with the percent sign "%" prefix. - * Valid values: A string containing a percent sign character "%", followed by 1 alphanumeric character, - * followed by 2 or more alphanumeric or underscore "_" characters but does not end with an underscore "_" - */ - public static final String PLUGIN_HOST_VARIABLE_NAME_MATCH_EXPRESSION = "%[a-zA-Z0-9][a-zA-Z0-9_]{2,}(? arguments_list, String workingDirectory, boolean inBackground, HashMap additionalExtras) { - if (label == null) label = "Execution Intent"; + public static void processPluginExecutionCommandError(final Context context, String logTag, final ExecutionCommand executionCommand) { + if(context == null || executionCommand == null) return; - StringBuilder executionIntentDump = new StringBuilder(); + if(executionCommand.errCode == null || executionCommand.errCode == 0) { + Logger.logWarn(LOG_TAG, "Ignoring call to processPluginExecutionCommandError() since the execution command errCode has not been set to a non-zero value"); + return; + } - executionIntentDump.append(label).append(":\n"); - executionIntentDump.append("Executable: `").append(executable).append("`\n"); - executionIntentDump.append("Arguments:").append(getArgumentsStringForLog(arguments_list)).append("\n"); - executionIntentDump.append("Working Directory: `").append(workingDirectory).append("`\n"); - executionIntentDump.append("inBackground: `").append(inBackground).append("`"); + // Log the error and any exception + logTag = TextDataUtils.getDefaultIfNull(logTag, LOG_TAG); + Logger.logStackTracesWithMessage(logTag, executionCommand.errmsg, executionCommand.throwableList); - if(additionalExtras != null) { - for (Map.Entry entry : additionalExtras.entrySet()) { - executionIntentDump.append("\n").append(entry.getKey()).append(": `").append(entry.getValue()).append("`"); - } - } + TermuxAppSharedPreferences preferences = new TermuxAppSharedPreferences(context); + // If user has disabled notifications for plugin, then just return + if (!preferences.getPluginErrorNotificationsEnabled()) + return; + + // Flash the errmsg + Logger.showToast(context, executionCommand.errmsg, true); - Logger.logMesssage(logLevel, logTag, executionIntentDump.toString()); + // Send a notification to show the errmsg which when clicked will open the {@link ReportActivity} + // to show the details of the error + String title = TermuxConstants.TERMUX_APP_NAME + " Plugin Execution Command Error"; + + Intent notificationIntent = ReportActivity.newInstance(context, new ReportInfo(UserAction.PLUGIN_EXECUTION_COMMAND, logTag, title, ExecutionCommand.getDetailedMarkdownString(executionCommand), true)); + PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + // Setup the notification channel if not already set up + setupPluginCommandErrorsNotificationChannel(context); + + // Use markdown in notification + CharSequence notifiationText = MarkdownUtils.getSpannedMarkdownText(context, executionCommand.errmsg); + //CharSequence notifiationText = executionCommand.errmsg; + + // Build the notification + Notification.Builder builder = getPluginCommandErrorsNotificationBuilder(context, title, notifiationText, notifiationText, pendingIntent, NotificationUtils.NOTIFICATION_MODE_VIBRATE); + if(builder == null) return; + + // Send the notification + int nextNotificationId = NotificationUtils.getNextNotificationId(context); + NotificationManager notificationManager = NotificationUtils.getNotificationManager(context); + if(notificationManager != null) + notificationManager.notify(nextNotificationId, builder.build()); } /** - * Converts arguments list to log friendly format. If arguments are null or of size 0, then - * nothing is returned. Otherwise following format is returned: + * Get {@link Notification.Builder} for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS} + * and {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}. * - * ``` - * Arg 0: `value` - * Arg 1: 'value` - * ``` - * - * @param arguments_list The arguments list. - * @return Returns the formatted arguments list. + * @param context The {@link Context} for operations. + * @param title The title for the notification. + * @param notifiationText The second line text of the notification. + * @param notificationBigText The full text of the notification that may optionally be styled. + * @param pendingIntent The {@link PendingIntent} which should be sent when notification is clicked. + * @param notificationMode The notification mode. It must be one of {@code NotificationUtils.NOTIFICATION_MODE_*}. + * @return Returns the {@link Notification.Builder}. */ - public static String getArgumentsStringForLog(List arguments_list) { - if (arguments_list==null || arguments_list.size() == 0) return ""; + @Nullable + public static Notification.Builder getPluginCommandErrorsNotificationBuilder(final Context context, final CharSequence title, final CharSequence notifiationText, final CharSequence notificationBigText, final PendingIntent pendingIntent, final int notificationMode) { - StringBuilder arguments_list_string = new StringBuilder("\n```\n"); - for(int i = 0; i != arguments_list.size(); i++) { - arguments_list_string.append("Arg ").append(i).append(": `").append(arguments_list.get(i)).append("`\n"); - } - arguments_list_string.append("```"); + Notification.Builder builder = NotificationUtils.geNotificationBuilder(context, + NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS, Notification.PRIORITY_HIGH, + title, notifiationText, notificationBigText, pendingIntent, notificationMode); + + if(builder == null) return null; + + // Enable timestamp + builder.setShowWhen(true); - return arguments_list_string.toString(); + // Set notification icon + builder.setSmallIcon(R.drawable.ic_error_notification); + + // Set background color for small notification icon + builder.setColor(0xFF607D8B); + + // Dismiss on click + builder.setAutoCancel(true); + + return builder; } + + /** + * Setup the notification channel for {@link #NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS} and + * {@link #NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS}. + * + * @param context The {@link Context} for operations. + */ + public static void setupPluginCommandErrorsNotificationChannel(final Context context) { + NotificationUtils.setupNotificationChannel(context, NOTIFICATION_CHANNEL_ID_PLUGIN_COMMAND_ERRORS, + NOTIFICATION_CHANNEL_NAME_PLUGIN_COMMAND_ERRORS, NotificationManager.IMPORTANCE_HIGH); + } + } diff --git a/app/src/main/java/com/termux/app/utils/TextDataUtils.java b/app/src/main/java/com/termux/app/utils/TextDataUtils.java index aebacdaea5..fdde1b072c 100644 --- a/app/src/main/java/com/termux/app/utils/TextDataUtils.java +++ b/app/src/main/java/com/termux/app/utils/TextDataUtils.java @@ -97,6 +97,19 @@ public static float rangedOrDefault(float value, float def, float min, float max + /** + * 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 getDefaultIfNull(@androidx.annotation.Nullable T object, @androidx.annotation.Nullable T def) { + return (object == null) ? def : object; + } + + + public static LinkedHashSet extractUrls(String text) { StringBuilder regex_sb = new StringBuilder(); diff --git a/app/src/main/res/drawable/ic_error_notification.xml b/app/src/main/res/drawable/ic_error_notification.xml new file mode 100644 index 0000000000..67f1771256 --- /dev/null +++ b/app/src/main/res/drawable/ic_error_notification.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21947fa525..4226e32908 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,7 +16,8 @@ &TERMUX_APP_NAME; &TERMUX_APP_NAME; user Run commands in &TERMUX_APP_NAME; environment - execute arbitrary commands within &TERMUX_APP_NAME; environment + execute arbitrary commands within &TERMUX_APP_NAME; + environment New session Failsafe Keyboard @@ -85,9 +86,10 @@ Validating file existence and permissions fafiled: \"%1$s\"\nException: %2$s Validating directory existence and permissions fafiled: \"%1$s\"\nException: %2$s - Invalid intent action to RunCommandService: \"%1$s\" - Invalid coommand path to RunCommandService: \"%1$s\" - RunCommandService require allow-external-apps property to be set to \"true\" in &TERMUX_PROPERTIES_PRIMARY_PATH_SHORT; file. + Invalid intent action to RunCommandService: `%1$s` + Invalid coommand path to RunCommandService: `%1$s` + RunCommandService require `allow-external-apps` property to be set to `true` in `&TERMUX_PROPERTIES_PRIMARY_PATH_SHORT;` file. + Visit https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/app/RunCommandService.java for more info on RUN_COMMAND Intent usage. Share Share With From 1b5e5b56cbf19809df09b4e90170d2962b4ca0cc Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 03:59:18 +0500 Subject: [PATCH 267/912] Partially integrate ExectionCommand into TermuxService and BackgroundJob The TERMUX_SERVICE.ACTION_SERVICE_EXECUTE intent received will be managed by the ExectionCommand now. The cwd and failsafe have been renamed to workingDirectory and isFailsafe. --- .../java/com/termux/app/BackgroundJob.java | 50 ++++++---- .../java/com/termux/app/TermuxActivity.java | 4 +- .../java/com/termux/app/TermuxService.java | 93 +++++++++++-------- .../app/terminal/TermuxSessionClient.java | 4 +- 4 files changed, 87 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java index 61e32317e8..171fedffa1 100644 --- a/app/src/main/java/com/termux/app/BackgroundJob.java +++ b/app/src/main/java/com/termux/app/BackgroundJob.java @@ -7,6 +7,8 @@ import com.termux.BuildConfig; import com.termux.app.utils.Logger; +import com.termux.models.ExecutionCommand; +import com.termux.models.ExecutionCommand.ExecutionState; import java.io.BufferedReader; import java.io.File; @@ -26,28 +28,33 @@ */ public final class BackgroundJob { - final Process mProcess; + Process mProcess; private static final String LOG_TAG = "BackgroundJob"; - public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service){ - this(cwd, fileToExecute, args, service, null); + public BackgroundJob(String executable, final String[] arguments, String workingDirectory, final TermuxService service){ + this(new ExecutionCommand(TermuxService.getNextExecutionId(), executable, arguments, workingDirectory, false, false), service); } - public BackgroundJob(String cwd, String fileToExecute, final String[] args, final TermuxService service, PendingIntent pendingIntent) { - String[] env = buildEnvironment(false, cwd); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; + public BackgroundJob(ExecutionCommand executionCommand, final TermuxService service) { + String[] env = buildEnvironment(false, executionCommand.workingDirectory); - final String[] progArray = setupProcessArgs(fileToExecute, args); - final String processDescription = Arrays.toString(progArray); + if (executionCommand.workingDirectory == null || executionCommand.workingDirectory.isEmpty()) + executionCommand.workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH; + + final String[] commandArray = setupProcessArgs(executionCommand.executable, executionCommand.arguments); + final String commandDescription = Arrays.toString(commandArray); + + if(!executionCommand.setState(ExecutionState.EXECUTING)) + return; Process process; try { - process = Runtime.getRuntime().exec(progArray, env, new File(cwd)); + process = Runtime.getRuntime().exec(commandArray, env, new File(executionCommand.workingDirectory)); } catch (IOException e) { mProcess = null; // TODO: Visible error message? - Logger.logStackTraceWithMessage(LOG_TAG, "Failed running background job: " + processDescription, e); + Logger.logStackTraceWithMessage(LOG_TAG, "Failed running background job: " + commandDescription, e); return; } @@ -79,7 +86,7 @@ public void run() { new Thread() { @Override public void run() { - Logger.logDebug(LOG_TAG, "[" + pid + "] starting: " + processDescription); + Logger.logDebug(LOG_TAG, "[" + pid + "] starting: " + commandDescription); InputStream stdout = mProcess.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(stdout, StandardCharsets.UTF_8)); @@ -109,16 +116,21 @@ public void run() { errThread.join(); result.putString("stderr", errResult.toString()); + if(!executionCommand.setState(ExecutionState.EXECUTED)) + return; + Intent data = new Intent(); data.putExtra("result", result); - if(pendingIntent != null) { + if(executionCommand.pluginPendingIntent != null) { try { - pendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data); + executionCommand.pluginPendingIntent.send(service.getApplicationContext(), Activity.RESULT_OK, data); } catch (PendingIntent.CanceledException e) { // The caller doesn't want the result? That's fine, just ignore } } + + executionCommand.setState(ExecutionState.SUCCESS); } catch (InterruptedException e) { // Ignore } @@ -133,10 +145,10 @@ private static void addToEnvIfPresent(List environment, String name) { } } - static String[] buildEnvironment(boolean failSafe, String cwd) { + static String[] buildEnvironment(boolean isFailSafe, String workingDirectory) { TermuxConstants.TERMUX_HOME_DIR.mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; + if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH; List environment = new ArrayList<>(); @@ -159,13 +171,13 @@ static String[] buildEnvironment(boolean failSafe, String cwd) { addToEnvIfPresent(environment, "ANDROID_RUNTIME_ROOT"); addToEnvIfPresent(environment, "ANDROID_TZDATA_ROOT"); - if (failSafe) { + 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=" + cwd); + environment.add("PWD=" + workingDirectory); environment.add("TMPDIR=" + TermuxConstants.TERMUX_TMP_PREFIX_DIR_PATH); } @@ -186,7 +198,7 @@ public static int getPid(Process p) { } } - static String[] setupProcessArgs(String fileToExecute, String[] args) { + static String[] setupProcessArgs(String fileToExecute, String[] arguments) { // The file to execute may either be: // - An elf file, in which we execute it directly. // - A script file without shebang, which we execute with our standard shell $PREFIX/bin/sh instead of the @@ -236,7 +248,7 @@ static String[] setupProcessArgs(String fileToExecute, String[] args) { List result = new ArrayList<>(); if (interpreter != null) result.add(interpreter); result.add(fileToExecute); - if (args != null) Collections.addAll(result, args); + if (arguments != null) Collections.addAll(result, arguments); return result.toArray(new String[0]); } diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index da66bddd86..b86db958fd 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -263,8 +263,8 @@ public void onServiceConnected(ComponentName componentName, IBinder service) { Intent i = getIntent(); if (i != null && Intent.ACTION_RUN.equals(i.getAction())) { // Android 7.1 app shortcut from res/xml/shortcuts.xml. - boolean failSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); - mTermuxSessionClient.addNewSession(failSafe, null); + boolean isFailSafe = i.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); + mTermuxSessionClient.addNewSession(isFailSafe, null); } else { mTermuxSessionClient.setCurrentSession(mTermuxSessionClient.getCurrentStoredSessionOrLast()); } diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index 8c8f8f3a9e..ba1e56dcfe 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -18,7 +18,6 @@ import android.os.IBinder; import android.os.PowerManager; import android.provider.Settings; -import android.util.Log; import android.widget.ArrayAdapter; import com.termux.R; @@ -28,16 +27,15 @@ import com.termux.app.terminal.TermuxSessionClient; import com.termux.app.terminal.TermuxSessionClientBase; import com.termux.app.utils.Logger; -import com.termux.app.utils.PluginUtils; import com.termux.app.utils.TextDataUtils; +import com.termux.models.ExecutionCommand; +import com.termux.models.ExecutionCommand.ExecutionState; import com.termux.terminal.TerminalEmulator; import com.termux.terminal.TerminalSession; import com.termux.terminal.TerminalSessionClient; import java.io.File; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; /** @@ -55,7 +53,9 @@ public final class TermuxService extends Service { private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel"; - private static final int NOTIFICATION_ID = 1337; + public static final int NOTIFICATION_ID = 1337; + + private static int EXECUTION_ID = 1000; /** This service is only bound from inside the same process and never uses IPC. */ class LocalBinder extends Binder { @@ -275,36 +275,45 @@ private void actionReleaseWakeLock(boolean updateNotification) { /** Process action to execute a shell command in a foreground session or in background. */ private void actionServiceExecute(Intent intent) { - Uri executableUri = intent.getData(); - String executablePath = (executableUri == null ? null : executableUri.getPath()); + if (intent == null){ + Logger.logError(LOG_TAG, "Ignoring null intent to actionServiceExecute"); + return; + } - String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS)); - String cwd = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR); + ExecutionCommand executionCommand = new ExecutionCommand(getNextExecutionId()); - PendingIntent pendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); + executionCommand.executableUri = intent.getData(); - int sessionAction = TextDataUtils.getIntStoredAsStringFromBundle(intent.getExtras(), - TERMUX_SERVICE.EXTRA_SESSION_ACTION, TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY); + if(executionCommand.executableUri != null) { + executionCommand.executable = executionCommand.executableUri.getPath(); + executionCommand.arguments = intent.getStringArrayExtra(TERMUX_SERVICE.EXTRA_ARGUMENTS); + } - if (intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false)) { - executeBackgroundCommand(executablePath, arguments, cwd, pendingIntent); + executionCommand.workingDirectory = intent.getStringExtra(TERMUX_SERVICE.EXTRA_WORKDIR); + executionCommand.inBackground = intent.getBooleanExtra(TERMUX_SERVICE.EXTRA_BACKGROUND, false); + executionCommand.isFailsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); + executionCommand.sessionAction = intent.getStringExtra(TERMUX_SERVICE.EXTRA_SESSION_ACTION); + executionCommand.commandLabel = TextDataUtils.getDefaultIfNull(intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_LABEL), "Execution Intent Command"); + executionCommand.commandDescription = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_DESCRIPTION); + executionCommand.commandHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_COMMAND_HELP); + executionCommand.pluginAPIHelp = intent.getStringExtra(TERMUX_SERVICE.EXTRA_PLUGIN_API_HELP); + executionCommand.isPluginExecutionCommand = true; + executionCommand.pluginPendingIntent = intent.getParcelableExtra(TERMUX_SERVICE.EXTRA_PENDING_INTENT); + + if (executionCommand.inBackground) { + executeBackgroundCommand(executionCommand); } else { - executeForegroundCommand(intent, executablePath, arguments, cwd, sessionAction); + executeForegroundCommand(executionCommand); } } /** Execute a shell command in background with {@link BackgroundJob}. */ - private void executeBackgroundCommand(String executablePath, String[] arguments, String cwd, PendingIntent pendingIntent) { + private void executeBackgroundCommand(ExecutionCommand executionCommand) { Logger.logDebug(LOG_TAG, "Starting background command"); - final String pendingIntentCreator; - if(pendingIntent != null) pendingIntentCreator = pendingIntent.getCreatorPackage(); else pendingIntentCreator = null; + Logger.logDebug(LOG_TAG, executionCommand.toString()); - PluginUtils.dumpExecutionIntentToLog(Log.DEBUG, LOG_TAG, null, executablePath, Arrays.asList(arguments), cwd, true, new HashMap() {{ - put("pendingIntentCreator", pendingIntentCreator); - }}); - - BackgroundJob task = new BackgroundJob(cwd, executablePath, arguments, this, pendingIntent); + BackgroundJob task = new BackgroundJob(executionCommand, this); mBackgroundTasks.add(task); updateNotification(); @@ -319,22 +328,20 @@ public void onBackgroundJobExited(final BackgroundJob task) { } /** Execute a shell command in a foreground terminal session. */ - private void executeForegroundCommand(Intent intent, String executablePath, String[] arguments, String cwd, int sessionAction) { + private void executeForegroundCommand(ExecutionCommand executionCommand) { Logger.logDebug(LOG_TAG, "Starting foreground command"); - boolean failsafe = intent.getBooleanExtra(TERMUX_ACTIVITY.ACTION_FAILSAFE_SESSION, false); + if(!executionCommand.setState(ExecutionState.EXECUTING)) + return; - PluginUtils.dumpExecutionIntentToLog(Log.DEBUG, LOG_TAG, null, executablePath, Arrays.asList(arguments), cwd, false, new HashMap() {{ - put("sessionAction", sessionAction); - put("failsafe", failsafe); - }}); + Logger.logDebug(LOG_TAG, executionCommand.toString()); - TerminalSession newSession = createTerminalSession(executablePath, arguments, cwd, failsafe); + TerminalSession newSession = createTerminalSession(executionCommand.executable, executionCommand.arguments, executionCommand.workingDirectory, executionCommand.isFailsafe); // Transform executable path to session name, e.g. "/bin/do-something.sh" => "do something.sh". - if (executablePath != null) { - int lastSlash = executablePath.lastIndexOf('/'); - String name = (lastSlash == -1) ? executablePath : executablePath.substring(lastSlash + 1); + if (executionCommand.executable != null) { + int lastSlash = executionCommand.executable.lastIndexOf('/'); + String name = (lastSlash == -1) ? executionCommand.executable : executionCommand.executable.substring(lastSlash + 1); name = name.replace('-', ' '); newSession.mSessionName = name; } @@ -344,7 +351,7 @@ private void executeForegroundCommand(Intent intent, String executablePath, Stri if(mTermuxSessionClient != null) mTermuxSessionClient.terminalSessionListNotifyUpdated(); - handleSessionAction(sessionAction, newSession); + handleSessionAction(TextDataUtils.getIntFromString(executionCommand.sessionAction, TERMUX_SERVICE.VALUE_EXTRA_SESSION_ACTION_SWITCH_TO_NEW_SESSION_AND_OPEN_ACTIVITY), newSession); } private void setCurrentStoredSession(TerminalSession newSession) { @@ -390,16 +397,16 @@ private void startTermuxActivity() { } /** Create a terminal session. */ - public TerminalSession createTerminalSession(String executablePath, String[] arguments, String cwd, boolean failSafe) { + public TerminalSession createTerminalSession(String executablePath, String[] arguments, String workingDirectory, boolean isFailSafe) { TermuxConstants.TERMUX_HOME_DIR.mkdirs(); - if (cwd == null || cwd.isEmpty()) cwd = TermuxConstants.TERMUX_HOME_DIR_PATH; + if (workingDirectory == null || workingDirectory.isEmpty()) workingDirectory = TermuxConstants.TERMUX_HOME_DIR_PATH; - String[] env = BackgroundJob.buildEnvironment(failSafe, cwd); + String[] env = BackgroundJob.buildEnvironment(isFailSafe, workingDirectory); boolean isLoginShell = false; if (executablePath == null) { - if (!failSafe) { + if (!isFailSafe) { for (String shellBinary : new String[]{"login", "bash", "zsh"}) { File shellFile = new File(TermuxConstants.TERMUX_BIN_PREFIX_DIR_PATH, shellBinary); if (shellFile.canExecute()) { @@ -426,7 +433,7 @@ public TerminalSession createTerminalSession(String executablePath, String[] arg args[0] = processName; if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1); - TerminalSession session = new TerminalSession(executablePath, cwd, args, env, getTermuxSessionClient()); + TerminalSession session = new TerminalSession(executablePath, workingDirectory, args, env, getTermuxSessionClient()); mTerminalSessions.add(session); updateNotification(); @@ -569,8 +576,8 @@ private void setupNotificationChannel() { NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance); channel.setDescription(channelDescription); - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - manager.createNotificationChannel(channel); + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.createNotificationChannel(channel); } /** Update the shown foreground service notification after making any changes that affect it. */ @@ -593,4 +600,8 @@ public List getSessions() { return mTerminalSessions; } + synchronized public static int getNextExecutionId() { + return EXECUTION_ID++; + } + } diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java index 467b778bc8..4229cc9140 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java @@ -182,7 +182,7 @@ public void renameSession(final TerminalSession sessionToRename) { }, -1, null, -1, null, null); } - public void addNewSession(boolean failSafe, String sessionName) { + public void addNewSession(boolean isFailSafe, String sessionName) { if (mActivity.getTermuxService().getSessions().size() >= MAX_SESSIONS) { new AlertDialog.Builder(mActivity).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message) .setPositiveButton(android.R.string.ok, null).show(); @@ -196,7 +196,7 @@ public void addNewSession(boolean failSafe, String sessionName) { workingDirectory = currentSession.getCwd(); } - TerminalSession newSession = mActivity.getTermuxService().createTerminalSession(null, null, workingDirectory, failSafe); + TerminalSession newSession = mActivity.getTermuxService().createTerminalSession(null, null, workingDirectory, isFailSafe); if (sessionName != null) { newSession.mSessionName = sessionName; } From 92b804dc9cd7186228f59b7dceaba5fc93240c2f Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 04:55:10 +0500 Subject: [PATCH 268/912] Add logging for termux bootstrap package installation and setup of storage symlinks --- .../java/com/termux/app/TermuxInstaller.java | 47 +++++++++++++------ .../java/com/termux/app/TermuxService.java | 2 +- app/src/main/res/values/strings.xml | 6 +-- 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java index 6b218a4f4a..389aaa60e5 100644 --- a/app/src/main/java/com/termux/app/TermuxInstaller.java +++ b/app/src/main/java/com/termux/app/TermuxInstaller.java @@ -28,11 +28,11 @@ * Install the Termux bootstrap packages if necessary by following the below steps: *

* (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a - * broken $PREFIX folder below. + * broken $PREFIX directory below. *

* (2) A progress dialog is shown with "Installing..." message and a spinner. *

- * (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below. + * (3) A staging directory, $STAGING_PREFIX, is {@link #deleteDirectory(File)} if left over from broken installation below. *

* (4) The zip file is loaded from a shared library. *

@@ -49,16 +49,21 @@ final class TermuxInstaller { /** Performs setup if necessary. */ static void setupIfNeeded(final Activity activity, final Runnable whenDone) { + Logger.logInfo(LOG_TAG, "Installing " + TermuxConstants.TERMUX_APP_NAME + " bootstrap packages."); + // Termux can only be run as the primary user (device owner) since only that // account has the expected file system paths. Verify that: UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE); boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0; if (!isPrimaryUser) { - new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message) + String bootstrapErrorMessage = activity.getString(R.string.bootstrap_error_not_primary_user_message, TermuxConstants.TERMUX_PREFIX_DIR_PATH); + Logger.logError(LOG_TAG, bootstrapErrorMessage); + new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(bootstrapErrorMessage) .setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show(); return; } + Logger.logInfo(LOG_TAG, "Creating prefix directory \"" + TermuxConstants.TERMUX_PREFIX_DIR_PATH + "\"."); final File PREFIX_FILE = TermuxConstants.TERMUX_PREFIX_DIR; if (PREFIX_FILE.isDirectory()) { whenDone.run(); @@ -74,9 +79,12 @@ public void run() { final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH); if (STAGING_PREFIX_FILE.exists()) { - deleteFolder(STAGING_PREFIX_FILE); + Logger.logInfo(LOG_TAG, "Deleting prefix staging directory \"" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "\"."); + deleteDirectory(STAGING_PREFIX_FILE); } + Logger.logInfo(LOG_TAG, "Extracting bootstrap zip to prefix staging directory \"" + TermuxConstants.TERMUX_STAGING_PREFIX_DIR_PATH + "\"."); + final byte[] buffer = new byte[8096]; final List> symlinks = new ArrayList<>(50); @@ -125,10 +133,13 @@ public void run() { Os.symlink(symlink.first, symlink.second); } + Logger.logInfo(LOG_TAG, "Moving prefix staging to prefix directory."); + if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) { - throw new RuntimeException("Unable to rename staging folder"); + throw new RuntimeException("Moving prefix staging to prefix directory failed"); } + Logger.logInfo(LOG_TAG, "Bootstrap packages installed successfully."); activity.runOnUiThread(whenDone); } catch (final Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Bootstrap error", e); @@ -139,9 +150,9 @@ public void run() { dialog.dismiss(); activity.finish(); }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> { - dialog.dismiss(); - TermuxInstaller.setupIfNeeded(activity, whenDone); - }).show(); + dialog.dismiss(); + TermuxInstaller.setupIfNeeded(activity, whenDone); + }).show(); } catch (WindowManager.BadTokenException e1) { // Activity already dismissed - ignore. } @@ -173,14 +184,14 @@ public static byte[] loadZipBytes() { public static native byte[] getZip(); - /** Delete a folder and all its content or throw. Don't follow symlinks. */ - static void deleteFolder(File fileOrDirectory) throws IOException { + /** Delete a directory and all its content or throw. Don't follow symlinks. */ + static void deleteDirectory(File fileOrDirectory) throws IOException { if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) { File[] children = fileOrDirectory.listFiles(); if (children != null) { for (File child : children) { - deleteFolder(child); + deleteDirectory(child); } } } @@ -192,6 +203,9 @@ static void deleteFolder(File fileOrDirectory) throws IOException { static void setupStorageSymlinks(final Context context) { final String LOG_TAG = "termux-storage"; + + Logger.logInfo(LOG_TAG, "Setting up storage symlinks."); + new Thread() { public void run() { try { @@ -199,18 +213,20 @@ public void run() { if (storageDir.exists()) { try { - deleteFolder(storageDir); + deleteDirectory(storageDir); } catch (IOException e) { - Logger.logError(LOG_TAG, "Could not delete old $HOME/storage, " + e.getMessage()); + Logger.logStackTraceWithMessage(LOG_TAG, "Failed to delete old ~/storage directory", e); return; } } if (!storageDir.mkdirs()) { - Logger.logError(LOG_TAG, "Unable to mkdirs() for $HOME/storage"); + Logger.logError(LOG_TAG, "Unable to create ~/storage directory."); return; } + Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/shared, ~/storage/downloads, ~/storage/dcim, ~/storage/pictures, ~/storage/music and ~/storage/movies for directories in \"" + Environment.getExternalStorageDirectory().getAbsolutePath() + "\"."); + File sharedDir = Environment.getExternalStorageDirectory(); Os.symlink(sharedDir.getAbsolutePath(), new File(storageDir, "shared").getAbsolutePath()); @@ -235,9 +251,12 @@ public void run() { File dir = dirs[i]; if (dir == null) continue; String symlinkName = "external-" + i; + Logger.logInfo(LOG_TAG, "Setting up storage symlinks at ~/storage/" + symlinkName + " for \"" + dir.getAbsolutePath() + "\"."); Os.symlink(dir.getAbsolutePath(), new File(storageDir, symlinkName).getAbsolutePath()); } } + + Logger.logInfo(LOG_TAG, "Storage symlinks created successfully."); } catch (Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Error setting up link", e); } diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java index ba1e56dcfe..01feb00596 100644 --- a/app/src/main/java/com/termux/app/TermuxService.java +++ b/app/src/main/java/com/termux/app/TermuxService.java @@ -151,7 +151,7 @@ public void onDestroy() { if (termuxTmpDir.exists()) { try { - TermuxInstaller.deleteFolder(termuxTmpDir.getCanonicalFile()); + TermuxInstaller.deleteDirectory(termuxTmpDir.getCanonicalFile()); } catch (Exception e) { Logger.logStackTraceWithMessage(LOG_TAG, "Error while removing file at " + termuxTmpDir.getAbsolutePath(), e); } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4226e32908..73793195e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,12 +28,12 @@ Keep screen on Autofill password - Installing… - Unable to install + Installing bootstrap packages… + Unable to install bootstrap &TERMUX_APP_NAME; was unable to install the bootstrap packages. Abort Try again - &TERMUX_APP_NAME; can only be installed on the primary user account. + &TERMUX_APP_NAME; can only be run as the primary user.\nBootstrap binaries compiled for &TERMUX_APP_NAME; have hardcoded $PREFIX path and cannot be installed under any path other than \"%1$s\". Max terminals reached Close down existing ones before creating new. From 1bdf9bf2e3967315268c947fd98d48ebb7c1bf38 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 04:57:35 +0500 Subject: [PATCH 269/912] Change log level to warn from error when termux.properties file is missing --- .../com/termux/app/settings/properties/SharedProperties.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java index 87f32eaea7..d2dfc417ff 100644 --- a/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java +++ b/app/src/main/java/com/termux/app/settings/properties/SharedProperties.java @@ -230,7 +230,7 @@ public static Properties getPropertiesFromFile(Context context, File propertiesF Properties properties = new Properties(); if (propertiesFile == null) { - Logger.logError(LOG_TAG, "Not loading properties since file is null"); + Logger.logWarn(LOG_TAG, "Not loading properties since file is null"); return properties; } From b856e16998a136bedc03a766728160f55914dfea Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 05:13:00 +0500 Subject: [PATCH 270/912] Move activities and fragments to respective packages Move com.termux.TermuxSettingsActivity to com.termux.app.activities.SettingsActivity Move com.termux.TermuxHepActivity to com.termux.app.activities.HelpActivity Move com.termux.settings.DebuggingPreferencesFragment to com.termux.app.fragments.settings.DebuggingPreferencesFragment --- app/src/main/AndroidManifest.xml | 4 ++-- app/src/main/java/com/termux/app/TermuxActivity.java | 6 ++++-- .../HelpActivity.java} | 4 ++-- .../SettingsActivity.java} | 4 ++-- .../settings/DebuggingPreferencesFragment.java | 3 +-- app/src/main/res/xml/root_preferences.xml | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) rename app/src/main/java/com/termux/app/{TermuxHelpActivity.java => activities/HelpActivity.java} (96%) rename app/src/main/java/com/termux/app/{TermuxSettingsActivity.java => activities/SettingsActivity.java} (92%) rename app/src/main/java/com/termux/app/{ => fragments}/settings/DebuggingPreferencesFragment.java (97%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17d2c1b209..e986f0c4e2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -89,7 +89,7 @@ diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index b86db958fd..e5e1ed902b 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -32,6 +32,8 @@ import com.termux.R; import com.termux.app.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY; +import com.termux.app.activities.HelpActivity; +import com.termux.app.activities.SettingsActivity; import com.termux.app.settings.preferences.TermuxAppSharedPreferences; import com.termux.app.terminal.TermuxSessionsListViewController; import com.termux.app.terminal.io.TerminalToolbarViewPager; @@ -529,10 +531,10 @@ public boolean onContextItemSelected(MenuItem item) { showStylingDialog(); return true; case CONTEXT_MENU_HELP_ID: - startActivity(new Intent(this, TermuxHelpActivity.class)); + startActivity(new Intent(this, HelpActivity.class)); return true; case CONTEXT_MENU_SETTINGS_ID: - startActivity(new Intent(this, TermuxSettingsActivity.class)); + startActivity(new Intent(this, SettingsActivity.class)); return true; case CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON: toggleKeepScreenOn(); diff --git a/app/src/main/java/com/termux/app/TermuxHelpActivity.java b/app/src/main/java/com/termux/app/activities/HelpActivity.java similarity index 96% rename from app/src/main/java/com/termux/app/TermuxHelpActivity.java rename to app/src/main/java/com/termux/app/activities/HelpActivity.java index 0aa8a97ac0..fa32bfc4e2 100644 --- a/app/src/main/java/com/termux/app/TermuxHelpActivity.java +++ b/app/src/main/java/com/termux/app/activities/HelpActivity.java @@ -1,4 +1,4 @@ -package com.termux.app; +package com.termux.app.activities; import android.app.Activity; import android.content.ActivityNotFoundException; @@ -13,7 +13,7 @@ import android.widget.RelativeLayout; /** Basic embedded browser for viewing help pages. */ -public final class TermuxHelpActivity extends Activity { +public final class HelpActivity extends Activity { WebView mWebView; diff --git a/app/src/main/java/com/termux/app/TermuxSettingsActivity.java b/app/src/main/java/com/termux/app/activities/SettingsActivity.java similarity index 92% rename from app/src/main/java/com/termux/app/TermuxSettingsActivity.java rename to app/src/main/java/com/termux/app/activities/SettingsActivity.java index 74604c96c0..7111a7b98a 100644 --- a/app/src/main/java/com/termux/app/TermuxSettingsActivity.java +++ b/app/src/main/java/com/termux/app/activities/SettingsActivity.java @@ -1,4 +1,4 @@ -package com.termux.app; +package com.termux.app.activities; import android.os.Bundle; @@ -8,7 +8,7 @@ import com.termux.R; -public class TermuxSettingsActivity extends AppCompatActivity { +public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java b/app/src/main/java/com/termux/app/fragments/settings/DebuggingPreferencesFragment.java similarity index 97% rename from app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java rename to app/src/main/java/com/termux/app/fragments/settings/DebuggingPreferencesFragment.java index 9944cf6113..d7a3aa600c 100644 --- a/app/src/main/java/com/termux/app/settings/DebuggingPreferencesFragment.java +++ b/app/src/main/java/com/termux/app/fragments/settings/DebuggingPreferencesFragment.java @@ -1,4 +1,4 @@ -package com.termux.app.settings; +package com.termux.app.fragments.settings; import android.content.Context; import android.os.Bundle; @@ -11,7 +11,6 @@ import androidx.preference.PreferenceManager; import com.termux.R; -import com.termux.app.settings.preferences.TermuxPreferenceConstants; import com.termux.app.settings.preferences.TermuxAppSharedPreferences; import com.termux.app.utils.Logger; diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml index 3ea2b58322..39c731ebf8 100644 --- a/app/src/main/res/xml/root_preferences.xml +++ b/app/src/main/res/xml/root_preferences.xml @@ -3,6 +3,6 @@ + app:fragment="com.termux.app.fragments.settings.DebuggingPreferencesFragment"/> From 4eced52c5fa76ec39a123e0f41f1eec9de608252 Mon Sep 17 00:00:00 2001 From: agnostic-apollo Date: Wed, 24 Mar 2021 05:28:38 +0500 Subject: [PATCH 271/912] Fix xml files naming convention --- app/src/main/java/com/termux/app/TermuxActivity.java | 2 +- .../java/com/termux/app/activities/SettingsActivity.java | 2 +- .../app/terminal/TermuxSessionsListViewController.java | 6 +++--- .../termux/app/terminal/io/TerminalToolbarViewPager.java | 4 ++-- ...ound_black.xml => session_background_black_selected.xml} | 0 ...ssion_background.xml => session_background_selected.xml} | 0 app/src/main/res/layout/activity_report.xml | 2 +- .../layout/{settings_activity.xml => activity_settings.xml} | 0 .../res/layout/{termux_activity.xml => activity_termux.xml} | 0 ...ssions_list_item.xml => item_terminal_sessions_list.xml} | 2 +- .../res/layout/{toolbar_layout.xml => partial_toolbar.xml} | 0 ...a_keys_view.xml => view_terminal_toolbar_extra_keys.xml} | 0 ..._input_view.xml => view_terminal_toolbar_text_input.xml} | 0 13 files changed, 9 insertions(+), 9 deletions(-) rename app/src/main/res/drawable/{selected_session_background_black.xml => session_background_black_selected.xml} (100%) rename app/src/main/res/drawable/{selected_session_background.xml => session_background_selected.xml} (100%) rename app/src/main/res/layout/{settings_activity.xml => activity_settings.xml} (100%) rename app/src/main/res/layout/{termux_activity.xml => activity_termux.xml} (100%) rename app/src/main/res/layout/{terminal_sessions_list_item.xml => item_terminal_sessions_list.xml} (84%) rename app/src/main/res/layout/{toolbar_layout.xml => partial_toolbar.xml} (100%) rename app/src/main/res/layout/{terminal_toolbar_extra_keys_view.xml => view_terminal_toolbar_extra_keys.xml} (100%) rename app/src/main/res/layout/{terminal_toolbar_text_input_view.xml => view_terminal_toolbar_text_input.xml} (100%) diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index e5e1ed902b..b87ba14515 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -163,7 +163,7 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.termux_activity); + setContentView(R.layout.activity_termux); View content = findViewById(android.R.id.content); content.setOnApplyWindowInsetsListener((v, insets) -> { diff --git a/app/src/main/java/com/termux/app/activities/SettingsActivity.java b/app/src/main/java/com/termux/app/activities/SettingsActivity.java index 7111a7b98a..b30b1a5799 100644 --- a/app/src/main/java/com/termux/app/activities/SettingsActivity.java +++ b/app/src/main/java/com/termux/app/activities/SettingsActivity.java @@ -13,7 +13,7 @@ public class SettingsActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.settings_activity); + setContentView(R.layout.activity_settings); if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java index 1216193a59..9951b07132 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionsListViewController.java @@ -32,7 +32,7 @@ public class TermuxSessionsListViewController extends ArrayAdapter sessionList) { - super(activity.getApplicationContext(), R.layout.terminal_sessions_list_item, sessionList); + super(activity.getApplicationContext(), R.layout.item_terminal_sessions_list, sessionList); this.mActivity = activity; } @@ -43,7 +43,7 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { View sessionRowView = convertView; if (sessionRowView == null) { LayoutInflater inflater = mActivity.getLayoutInflater(); - sessionRowView = inflater.inflate(R.layout.terminal_sessions_list_item, parent, false); + sessionRowView = inflater.inflate(R.layout.item_terminal_sessions_list, parent, false); } TextView sessionTitleView = sessionRowView.findViewById(R.id.session_title); @@ -58,7 +58,7 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { if (isUsingBlackUI) { sessionTitleView.setBackground( - ContextCompat.getDrawable(mActivity, R.drawable.selected_session_background_black) + ContextCompat.getDrawable(mActivity, R.drawable.session_background_black_selected) ); } diff --git a/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java b/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java index e53fbbcd07..1e23220d5f 100644 --- a/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java +++ b/app/src/main/java/com/termux/app/terminal/io/TerminalToolbarViewPager.java @@ -42,7 +42,7 @@ public Object instantiateItem(@NonNull ViewGroup collection, int position) { LayoutInflater inflater = LayoutInflater.from(mActivity); View layout; if (position == 0) { - layout = inflater.inflate(R.layout.terminal_toolbar_extra_keys_view, collection, false); + layout = inflater.inflate(R.layout.view_terminal_toolbar_extra_keys, collection, false); ExtraKeysView extraKeysView = (ExtraKeysView) layout; mActivity.setExtraKeysView(extraKeysView); extraKeysView.reload(mActivity.getProperties().getExtraKeysInfo()); @@ -53,7 +53,7 @@ public Object instantiateItem(@NonNull ViewGroup collection, int position) { } } else { - layout = inflater.inflate(R.layout.terminal_toolbar_text_input_view, collection, false); + layout = inflater.inflate(R.layout.view_terminal_toolbar_text_input, collection, false); final EditText editText = layout.findViewById(R.id.terminal_toolbar_text_input); if(mSavedTextInput != null) { diff --git a/app/src/main/res/drawable/selected_session_background_black.xml b/app/src/main/res/drawable/session_background_black_selected.xml similarity index 100% rename from app/src/main/res/drawable/selected_session_background_black.xml rename to app/src/main/res/drawable/session_background_black_selected.xml diff --git a/app/src/main/res/drawable/selected_session_background.xml b/app/src/main/res/drawable/session_background_selected.xml similarity index 100% rename from app/src/main/res/drawable/selected_session_background.xml rename to app/src/main/res/drawable/session_background_selected.xml diff --git a/app/src/main/res/layout/activity_report.xml b/app/src/main/res/layout/activity_report.xml index b442a324d8..43fff0351d 100644 --- a/app/src/main/res/layout/activity_report.xml +++ b/app/src/main/res/layout/activity_report.xml @@ -5,7 +5,7 @@ android:orientation="vertical"> Date: Wed, 24 Mar 2021 06:15:45 +0500 Subject: [PATCH 272/912] Fix string resources naming convention --- app/src/main/AndroidManifest.xml | 4 +- .../com/termux/app/RunCommandService.java | 8 +- .../java/com/termux/app/TermuxActivity.java | 30 ++-- .../termux/app/activities/ReportActivity.java | 2 +- .../app/terminal/TermuxSessionClient.java | 4 +- .../termux/app/terminal/TermuxViewClient.java | 10 +- .../java/com/termux/app/utils/FileUtils.java | 28 +-- .../com/termux/app/utils/PluginUtils.java | 2 +- .../java/com/termux/app/utils/ShareUtils.java | 2 +- .../filepicker/TermuxDocumentsProvider.java | 4 +- .../TermuxFileReceiverActivity.java | 4 +- app/src/main/res/layout/activity_termux.xml | 4 +- app/src/main/res/menu/menu_report.xml | 4 +- app/src/main/res/values/strings.xml | 159 +++++++++++------- app/src/main/res/xml/shortcuts.xml | 4 +- 15 files changed, 152 insertions(+), 117 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e986f0c4e2..888d269458 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,9 +15,9 @@ diff --git a/app/src/main/java/com/termux/app/RunCommandService.java b/app/src/main/java/com/termux/app/RunCommandService.java index f8abb0a986..d91ed52cec 100644 --- a/app/src/main/java/com/termux/app/RunCommandService.java +++ b/app/src/main/java/com/termux/app/RunCommandService.java @@ -186,13 +186,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { runStartForeground(); ExecutionCommand executionCommand = new ExecutionCommand(); - executionCommand.pluginAPIHelp = this.getString(R.string.run_command_service_api_help); + executionCommand.pluginAPIHelp = this.getString(R.string.error_run_command_service_api_help); String errmsg; // If invalid action passed, then just return if (!RUN_COMMAND_SERVICE.ACTION_RUN_COMMAND.equals(intent.getAction())) { - errmsg = this.getString(R.string.run_command_service_invalid_action, intent.getAction()); + errmsg = this.getString(R.string.error_run_command_service_invalid_intent_action, intent.getAction()); executionCommand.setStateFailed(1, errmsg, null); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; @@ -227,7 +227,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { null, PluginUtils.PLUGIN_EXECUTABLE_FILE_PERMISSIONS, false, false); if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.executable_absolute_path, executionCommand.executable); + errmsg += "\n" + this.getString(R.string.msg_executable_absolute_path, executionCommand.executable); executionCommand.setStateFailed(1, errmsg, null); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; @@ -248,7 +248,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { true, true, false, true); if (errmsg != null) { - errmsg += "\n" + this.getString(R.string.working_directory_absolute_path, executionCommand.workingDirectory); + errmsg += "\n" + this.getString(R.string.msg_working_directory_absolute_path, executionCommand.workingDirectory); executionCommand.setStateFailed(1, errmsg, null); PluginUtils.processPluginExecutionCommandError(this, LOG_TAG, executionCommand); return Service.START_NOT_STICKY; diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java index b87ba14515..dd6f5058b6 100644 --- a/app/src/main/java/com/termux/app/TermuxActivity.java +++ b/app/src/main/java/com/termux/app/TermuxActivity.java @@ -395,8 +395,8 @@ private void setNewSessionButtonView() { View newSessionButton = findViewById(R.id.new_session_button); newSessionButton.setOnClickListener(v -> mTermuxSessionClient.addNewSession(false, null)); newSessionButton.setOnLongClickListener(v -> { - DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, null, R.string.session_new_named_positive_button, - text -> mTermuxSessionClient.addNewSession(false, text), R.string.new_session_failsafe, text -> mTermuxSessionClient.addNewSession(true, text) + DialogUtils.textInput(TermuxActivity.this, R.string.title_create_named_session, null, R.string.action_create_named_session_confirm, + text -> mTermuxSessionClient.addNewSession(false, text), R.string.action_new_session_failsafe, text -> mTermuxSessionClient.addNewSession(true, text) , -1, null, null); return true; }); @@ -489,15 +489,15 @@ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuIn } } - menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.select_url); - menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.select_all_and_share); - if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.autofill_password); - menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal); - menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning()); - menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.style_terminal); - menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.getKeepScreenOn()); - menu.add(Menu.NONE, CONTEXT_MENU_HELP_ID, Menu.NONE, R.string.help); - menu.add(Menu.NONE, CONTEXT_MENU_SETTINGS_ID, Menu.NONE, R.string.settings); + menu.add(Menu.NONE, CONTEXT_MENU_SELECT_URL_ID, Menu.NONE, R.string.action_select_url); + menu.add(Menu.NONE, CONTEXT_MENU_SHARE_TRANSCRIPT_ID, Menu.NONE, R.string.action_share_transcript); + if (addAutoFillMenu) menu.add(Menu.NONE, CONTEXT_MENU_AUTOFILL_ID, Menu.NONE, R.string.action_autofill_password); + menu.add(Menu.NONE, CONTEXT_MENU_RESET_TERMINAL_ID, Menu.NONE, R.string.action_reset_terminal); + menu.add(Menu.NONE, CONTEXT_MENU_KILL_PROCESS_ID, Menu.NONE, getResources().getString(R.string.action_kill_process, getCurrentSession().getPid())).setEnabled(currentSession.isRunning()); + menu.add(Menu.NONE, CONTEXT_MENU_STYLING_ID, Menu.NONE, R.string.action_style_terminal); + menu.add(Menu.NONE, CONTEXT_MENU_TOGGLE_KEEP_SCREEN_ON, Menu.NONE, R.string.action_toggle_keep_screen_on).setCheckable(true).setChecked(mPreferences.getKeepScreenOn()); + menu.add(Menu.NONE, CONTEXT_MENU_HELP_ID, Menu.NONE, R.string.action_open_help); + menu.add(Menu.NONE, CONTEXT_MENU_SETTINGS_ID, Menu.NONE, R.string.action_open_settings); } /** Hook system menu to show context menu instead. */ @@ -552,7 +552,7 @@ private void showKillSessionDialog(TerminalSession session) { final AlertDialog.Builder b = new AlertDialog.Builder(this); b.setIcon(android.R.drawable.ic_dialog_alert); - b.setMessage(R.string.confirm_kill_process); + b.setMessage(R.string.title_confirm_kill_process); b.setPositiveButton(android.R.string.yes, (dialog, id) -> { dialog.dismiss(); session.finishIfRunning(); @@ -564,7 +564,7 @@ private void showKillSessionDialog(TerminalSession session) { private void resetSession(TerminalSession session) { if (session != null) { session.reset(); - showToast(getResources().getString(R.string.reset_toast_notification), true); + showToast(getResources().getString(R.string.msg_terminal_reset), true); } } @@ -576,8 +576,8 @@ private void showStylingDialog() { } catch (ActivityNotFoundException | IllegalArgumentException e) { // The startActivity() call is not documented to throw IllegalArgumentException. // However, crash reporting shows that it sometimes does, so catch it here. - new AlertDialog.Builder(this).setMessage(getString(R.string.styling_not_installed)) - .setPositiveButton(R.string.styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/" + TermuxConstants.TERMUX_STYLING_PACKAGE_NAME + " /")))).setNegativeButton(android.R.string.cancel, null).show(); + new AlertDialog.Builder(this).setMessage(getString(R.string.error_styling_not_installed)) + .setPositiveButton(R.string.action_styling_install, (dialog, which) -> startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://f-droid.org/en/packages/" + TermuxConstants.TERMUX_STYLING_PACKAGE_NAME + " /")))).setNegativeButton(android.R.string.cancel, null).show(); } } private void toggleKeepScreenOn() { diff --git a/app/src/main/java/com/termux/app/activities/ReportActivity.java b/app/src/main/java/com/termux/app/activities/ReportActivity.java index 5b033b56dc..cf4ccdd346 100644 --- a/app/src/main/java/com/termux/app/activities/ReportActivity.java +++ b/app/src/main/java/com/termux/app/activities/ReportActivity.java @@ -129,7 +129,7 @@ public boolean onOptionsItemSelected(final MenuItem item) { int id = item.getItemId(); if (id == R.id.menu_item_share_report) { if (mReportInfo != null) - ShareUtils.shareText(this, getString(R.string.report_text), mReportInfo.reportString); + ShareUtils.shareText(this, getString(R.string.title_report_text), mReportInfo.reportString); } else if (id == R.id.menu_item_copy_report) { if (mReportInfo != null) ShareUtils.copyTextToClipboard(this, mReportInfo.reportString, null); diff --git a/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java b/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java index 4229cc9140..c3e4df7fd3 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxSessionClient.java @@ -176,7 +176,7 @@ public void switchToSession(boolean forward) { public void renameSession(final TerminalSession sessionToRename) { if (sessionToRename == null) return; - DialogUtils.textInput(mActivity, R.string.session_rename_title, sessionToRename.mSessionName, R.string.session_rename_positive_button, text -> { + DialogUtils.textInput(mActivity, R.string.title_rename_session, sessionToRename.mSessionName, R.string.action_rename_session_confirm, text -> { sessionToRename.mSessionName = text; terminalSessionListNotifyUpdated(); }, -1, null, -1, null, null); @@ -184,7 +184,7 @@ public void renameSession(final TerminalSession sessionToRename) { public void addNewSession(boolean isFailSafe, String sessionName) { if (mActivity.getTermuxService().getSessions().size() >= MAX_SESSIONS) { - new AlertDialog.Builder(mActivity).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message) + new AlertDialog.Builder(mActivity).setTitle(R.string.title_max_terminals_reached).setMessage(R.string.msg_max_terminals_reached) .setPositiveButton(android.R.string.ok, null).show(); } else { TerminalSession currentSession = mActivity.getCurrentSession(); diff --git a/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java index 6f3e29cb43..98d5d14675 100644 --- a/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java +++ b/app/src/main/java/com/termux/app/terminal/TermuxViewClient.java @@ -346,8 +346,8 @@ public void shareSessionTranscript() { intent.setType("text/plain"); transcriptText = TextDataUtils.getTruncatedCommandOutput(transcriptText, 100_000); intent.putExtra(Intent.EXTRA_TEXT, transcriptText); - intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.share_transcript_title)); - mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.share_transcript_chooser_title))); + intent.putExtra(Intent.EXTRA_SUBJECT, mActivity.getString(R.string.title_share_transcript)); + mActivity.startActivity(Intent.createChooser(intent, mActivity.getString(R.string.title_share_transcript_with))); } catch (Exception e) { Logger.logStackTraceWithMessage("Failed to get share session transcript of length " + transcriptText.length(), e); } @@ -361,7 +361,7 @@ public void showUrlSelection() { LinkedHashSet urlSet = TextDataUtils.extractUrls(text); if (urlSet.isEmpty()) { - new AlertDialog.Builder(mActivity).setMessage(R.string.select_url_no_found).show(); + new AlertDialog.Builder(mActivity).setMessage(R.string.title_select_url_none_found).show(); return; } @@ -373,8 +373,8 @@ public void showUrlSelection() { String url = (String) urls[which]; ClipboardManager clipboard = (ClipboardManager) mActivity.getSystemService(Context.CLIPBOARD_SERVICE); clipboard.setPrimaryClip(new ClipData(null, new String[]{"text/plain"}, new ClipData.Item(url))); - Toast.makeText(mActivity, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show(); - }).setTitle(R.string.select_url_dialog_title).create(); + Toast.makeText(mActivity, R.string.msg_select_url_copied_to_clipboard, Toast.LENGTH_LONG).show(); + }).setTitle(R.string.title_select_url_dialog).create(); // Long press to open URL: dialog.setOnShowListener(di -> { diff --git a/app/src/main/java/com/termux/app/utils/FileUtils.java b/app/src/main/java/com/termux/app/utils/FileUtils.java index f50ea91c59..194e37f920 100644 --- a/app/src/main/java/com/termux/app/utils/FileUtils.java +++ b/app/src/main/java/com/termux/app/utils/FileUtils.java @@ -156,14 +156,14 @@ public static boolean isPathInDirPath(String path, String dirPath, boolean ensur * failed, otherwise {@code null}. */ public static String validateRegularFileExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsUnderParentDirPath) { - if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_file); + if (path == null || path.isEmpty()) return context.getString(R.string.error_null_or_empty_file); try { File file = new File(path); // If file exits but not a regular file if (file.exists() && !file.isFile()) { - return context.getString(R.string.non_regular_file_found); + return context.getString(R.string.error_non_regular_file_found); } boolean isPathUnderParentDirPath = false; @@ -183,7 +183,7 @@ public static String validateRegularFileExistenceAndPermissions(final Context co // If path is not a regular file // Regular files cannot be automatically created so we do not ignore if missing if (!file.isFile()) { - return context.getString(R.string.no_regular_file_found); + return context.getString(R.string.error_no_regular_file_found); } // If there is not parentDirPath restriction or path is not under parentDirPath or @@ -197,7 +197,7 @@ public static String validateRegularFileExistenceAndPermissions(final Context co } // Some function calls may throw SecurityException, etc catch (Exception e) { - return context.getString(R.string.validate_file_existence_and_permissions_failed_with_exception, path, e.getMessage()); + return context.getString(R.string.error_validate_file_existence_and_permissions_failed_with_exception, path, e.getMessage()); } return null; @@ -230,14 +230,14 @@ public static String validateRegularFileExistenceAndPermissions(final Context co * failed, otherwise {@code null}. */ public static String validateDirectoryExistenceAndPermissions(final Context context, final String path, final String parentDirPath, String permissionsToCheck, final boolean createDirectoryIfMissing, final boolean setMissingPermissions, final boolean ignoreErrorsIfPathIsInParentDirPath, final boolean ignoreIfNotExecutable) { - if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_directory); + if (path == null || path.isEmpty()) return context.getString(R.string.error_null_or_empty_directory); try { File file = new File(path); // If file exits but not a directory file if (file.exists() && !file.isDirectory()) { - return context.getString(R.string.non_directory_file_found); + return context.getString(R.string.error_non_directory_file_found); } boolean isPathInParentDirPath = false; @@ -254,7 +254,7 @@ public static String validateDirectoryExistenceAndPermissions(final Context cont Logger.logVerbose(LOG_TAG, "Creating missing directory at path: \"" + path + "\""); // If failed to create directory if (!file.mkdirs()) { - return context.getString(R.string.creating_missing_directory_failed, path); + return context.getString(R.string.error_creating_missing_directory_failed, path); } } @@ -271,7 +271,7 @@ public static String validateDirectoryExistenceAndPermissions(final Context cont // If path is not a directory // Directories can be automatically created so we can ignore if missing with above check if (!file.isDirectory()) { - return context.getString(R.string.no_directory_found); + return context.getString(R.string.error_no_directory_found); } if (permissionsToCheck != null) { @@ -282,7 +282,7 @@ public static String validateDirectoryExistenceAndPermissions(final Context cont } // Some function calls may throw SecurityException, etc catch (Exception e) { - return context.getString(R.string.validate_directory_existence_and_permissions_failed_with_exception, path, e.getMessage()); + return context.getString(R.string.error_validate_directory_existence_and_permissions_failed_with_exception, path, e.getMessage()); } return null; @@ -332,11 +332,11 @@ public static void setMissingFilePermissions(String path, String permissionsToSe * @return Returns the {@code errmsg} if validating permissions failed, otherwise {@code null}. */ public static String checkMissingFilePermissions(Context context, String path, String permissionsToCheck, String fileType, boolean ignoreIfNotExecutable) { - if (path == null || path.isEmpty()) return context.getString(R.string.null_or_empty_path); + if (path == null || path.isEmpty()) return context.getString(R.string.error_null_or_empty_path); if (!isValidPermissingString(permissionsToCheck)) { Logger.logError(LOG_TAG, "Invalid permissionsToCheck passed to checkMissingFilePermissions: \"" + permissionsToCheck + "\""); - return context.getString(R.string.invalid_file_permissions_string_to_check); + return context.getString(R.string.error_invalid_file_permissions_string_to_check); } if (fileType == null || fileType.isEmpty()) fileType = "File"; @@ -345,17 +345,17 @@ public static String checkMissingFilePermissions(Context context, String path, S // If file is not readable if (permissionsToCheck.contains("r") && !file.canRead()) { - return context.getString(R.string.file_not_readable, fileType); + return context.getString(R.string.error_file_not_readable, fileType); } // If file is not writable if (permissionsToCheck.contains("w") && !file.canWrite()) { - return context.getString(R.string.file_not_writable, fileType); + return context.getString(R.string.error_file_not_writable, fileType); } // If file is not executable // This canExecute() will give "avc: granted { execute }" warnings for target sdk 29 else if (permissionsToCheck.contains("x") && !file.canExecute() && !ignoreIfNotExecutable) { - return context.getString(R.string.file_not_executable, fileType); + return context.getString(R.string.error_file_not_executable, fileType); } return null; diff --git a/app/src/main/java/com/termux/app/utils/PluginUtils.java b/app/src/main/java/com/termux/app/utils/PluginUtils.java index 0fd2a7e9c1..387c156046 100644 --- a/app/src/main/java/com/termux/app/utils/PluginUtils.java +++ b/app/src/main/java/com/termux/app/utils/PluginUtils.java @@ -96,7 +96,7 @@ public static void sendExecuteResultToResultsService(final Context context, fina public static String checkIfRunCommandServiceAllowExternalAppsPolicyIsViolated(final Context context) { String errmsg = null; if (!SharedProperties.isPropertyValueTrue(context, TermuxPropertyConstants.getTermuxPropertiesFile(), TermuxConstants.PROP_ALLOW_EXTERNAL_APPS)) { - errmsg = context.getString(R.string.run_command_service_allow_external_apps_ungranted_warning); + errmsg = context.getString(R.string.error_run_command_service_allow_external_apps_ungranted); } return errmsg; diff --git a/app/src/main/java/com/termux/app/utils/ShareUtils.java b/app/src/main/java/com/termux/app/utils/ShareUtils.java index 4f8b628e22..07b961cd0f 100644 --- a/app/src/main/java/com/termux/app/utils/ShareUtils.java +++ b/app/src/main/java/com/termux/app/utils/ShareUtils.java @@ -43,7 +43,7 @@ public static void shareText(final Context context, final String subject, final shareTextIntent.putExtra(Intent.EXTRA_SUBJECT, subject); shareTextIntent.putExtra(Intent.EXTRA_TEXT, text); - openSystemAppChooser(context, shareTextIntent, context.getString(R.string.share_with)); + openSystemAppChooser(context, shareTextIntent, context.getString(R.string.title_share_with)); } /** diff --git a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java index 3bb1433423..0a4de74f69 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java +++ b/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java @@ -22,7 +22,7 @@ /** * A document provider for the Storage Access Framework which exposes the files in the - * $HOME/ folder to other apps. + * $HOME/ directory to other apps. *

* Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent: *

@@ -167,7 +167,7 @@ public Cursor querySearchDocuments(String rootId, String query, String[] project final int MAX_SEARCH_RESULTS = 50; while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) { final File file = pending.removeFirst(); - // Avoid folders outside the $HOME folders linked in to symlinks (to avoid e.g. search + // Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g. search // through the whole SD card). boolean isInsideHome; try { diff --git a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java index 94194f27a4..8a279fcae1 100644 --- a/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java +++ b/app/src/main/java/com/termux/filepicker/TermuxFileReceiverActivity.java @@ -118,7 +118,7 @@ void handleContentUri(final Uri uri, String subjectFromIntent) { } void promptNameAndSave(final InputStream in, final String attachmentFileName) { - DialogUtils.textInput(this, R.string.file_received_title, attachmentFileName, R.string.file_received_edit_button, text -> { + DialogUtils.textInput(this, R.string.title_file_received, attachmentFileName, R.string.action_file_received_edit, text -> { File outFile = saveStreamWithName(in, text); if (outFile == null) return; @@ -141,7 +141,7 @@ void promptNameAndSave(final InputStream in, final String attachmentFileName) { startService(executeIntent); finish(); }, - R.string.file_received_open_folder_button, text -> { + R.string.action_file_received_open_directory, text -> { if (saveStreamWithName(in, text) == null) return; Intent executeIntent = new Intent(TERMUX_SERVICE.ACTION_SERVICE_EXECUTE); diff --git a/app/src/main/res/layout/activity_termux.xml b/app/src/main/res/layout/activity_termux.xml index 2faa7d3120..604d17b4b4 100644 --- a/app/src/main/res/layout/activity_termux.xml +++ b/app/src/main/res/layout/activity_termux.xml @@ -56,7 +56,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/toggle_soft_keyboard" /> + android:text="@string/action_toggle_soft_keyboard" />