Termux is a terminal emulator for Android combined with a collection of packages for command line software. This help
+ explains both the terminal interface and the packaging tool available from inside the terminal.
+
Want to ask a question, report a bug or have an idea for a new package or feature?
+ Visit the Google+ Termux Community!
+
+
User interface
+
At launch Termux shows a terminal interface, whose text size can be adjusted by pinch zooming or double tapping
+ and pulling the content towards or from you.
+
Besides the terminal (with keyboard shortcuts explained below) there are three additional interface elements available:
+ A context menu, navigation drawer
+ and notification.
+
The context menu can be shown by long pressing anywhere on the terminal. It provides menu entries for:
+
+
Selecting and pasting text.
+
Sharing text from the terminal to other apps (e.g. email or SMS)
+
Resetting the terminal if it gets stuck.
+
Switching the terminal to full-screen.
+
Hangup (exiting the current terminal session).
+
Styling the terminal by selecting a font and a color scheme.
+
Showing this help page.
+
+
The navigation drawer is revealed by swiping from the left part of the screen. It has three
+ elements:
+
+
A list of sessions. Clicking on a session shows it in the terminal while long pressing allows you to specify a session title.
+
A button to toggle visibility of the touch keyboard.
+
A button to create new terminal sessions (long press for creating a named session or a fail-safe one).
+
+
The notification, available when a terminal session is running, is available by pulling down the notification menu.
+ Pressing the notification leads to the most current terminal session. The notification may also be expanded
+ (by pinch-zooming or performing a single-finger glide) to expose three actions:
+
+
Exiting all running terminal sessions.
+
Use a wake lock to avoid entering sleep mode.
+
Use a high performance wifi lock to maximize wifi performance.
+
+
With a wake or wifi lock held the notification and Termux background processes will be available even if no terminal
+ session is running, which allows server and other background processes to run more reliably.
+
+
Using a touch keyboard
+
Using the Ctrl key is necessary for working with a terminal - but most touch keyboards
+ does not include one. For that purpose Termux uses the Volume down button to emulate
+ the Ctrl key. For example, pressing Volume down+L on a touch keyboard sends the same input as
+ pressing Ctrl+L on a hardware keyboard. The result of using Ctrl in combination
+ with a key depends on which program is used, but for many command line tools the following
+ shortcuts works:
+
+
Ctrl+A → Move cursor to the beginning of line.
+
Ctrl+C → Abort (send SIGINT to) current process.
+
Ctrl+D → Logout of a terminal session.
+
Ctrl+E → Move cursor to the end of line.
+
Ctrl+K → Delete from cursor to the end of line.
+
Ctrl+L → Clear the terminal.
+
Ctrl+Z → Suspend (send SIGTSTP to) current process.
+
+
The Volume up key also serves as a special key to produce certain input:
+
+
Volume Up+L → | (the pipe character).
+
Volume Up+E → Escape key.
+
Volume Up+T → Tab key.
+
Volume Up+1 → F1 (and Volume Up+2 → F2, etc).
+
Volume Up+B → Alt+B, back a word when using readline.
+
Volume Up+F → Alt+F, forward a word when using readline.
+
Volume Up+W → Up arrow key.
+
Volume Up+A → Left arrow key.
+
Volume Up+S → Down arrow key.
+
Volume Up+D → Right arrow key.
+
+
+
Using a hardware keyboard
+
The following shortcuts are available when using Termux with a hardware (e.g. bluetooth) keyboard by combining them with Ctrl+Shift:
+
+
'C' → Create new session
+
'R' → Rename current session
+
Down arrow (or 'N') → Next session
+
Up arrow (or 'P') → Previous session
+
Right arrow → Open drawer
+
Left arrow → Close drawer
+
'F' → Toggle full screen
+
'M' → Show menu
+
'V' → Paste
+
+/- → Adjust text size
+
1-9 → Go to numbered session
+
+
+
Package management
+
A minimal base system consisting of the Apt package manager and the busybox collection of system utilities
+ is installed when first starting Termux. Additional packages are available using the apt command:
+
+
apt update
Updates the list of available packages. This commands needs to be run initially directly after installation
+ and regularly afterwards to receive updates.
+
apt search <query>
Search among available packages.
+
apt install <package>
Install a new package.
+
apt upgrade
Upgrade outdated packages. For Apt to know about newer packages you will need to update the package index, so you will normally want to run apt update before upgrading.
+
apt show <package>
Show information about a package.
+
apt list
List all available packages.
+
apt list --installed
List all installed packages.
+
apt remove <package>
Remove an installed package.
+
+
+
Apt as a package manager uses a package format named dpkg. Normally direct use of dpkg is not necessary, but the
+ following two commands may be of use:
+
+
dpkg -L <package>
+
List installed files of a package.
+
dpkg --verify
+
Verify the integrity of installed packages.
+
+
View the apt manual page (execute apt install man to install a man page viewer first) for more information.
+
+
Text editing
+
By default the busybox version of vi is available. This is a barebone and somewhat unfriendly editor -
+ install nano for a more straight-forward editor and
+ vim for a more powerful one.
+
+
Using SSH
+
By installing the openssh package (by executing apt install openssh) you may SSH into remote systems,
+ optionally putting private keys or configuration under $HOME/.ssh/.
+
If you wish to use an SSH agent to avoid entering passwords, the Termux openssh package provides
+ a wrapper script named ssha (note the 'a' at the end) for ssh which:
+
+
Starts the ssh agent if necessary (or connect to it if already running).
+
Runs ssh-add if necessary.
+
Runs ssh with the provided arguments.
+
+
This means that the agent will prompt for a key password at first run, but remember the authorization for subsequent ones.
+
+
Interactive shells
+
The base system that is installed when first starting Termux uses the bash shell while zsh is available as
+ an installable alternative:
zsh - a powerful shell with information available at
+ A User's Guide to the Z-Shell, the
+ Z Shell Manual or
+ ZSH Tips by ZZapper.
+ After installing zsh through apt install zsh, execute chsh -s zsh to set it as the default login shell when starting Termux
+ (and change back with chsh -s bash if necessary).
+
+
+
Termux and Android
+
Termux is designed to cope with the restrictions of running as an ordinary Android app without requiring root, which
+ leads to several differences between Termux and a traditional desktop system. The file system layout is drastically different:
+
+
Common folders such as /bin, /usr/, /var and /etc does not exist.
+
The Android system provides a basic non-standard file system hierarchy, where e.g. /system/bin contains some system binaries.
+
The user folder $HOME is inside the private file area exposed to Termux as an ordinary Android app.
+ Uninstalling Termux will cause this file area to be wiped - so save important files outside this area such as in /sdcard
+ or use a version control system such as git.
+
Termux installs its packages in a folder exposed through the $PREFIX environment variable (with e.g. binaries in $PREFIX/bin,
+ and configuration in $PREFIX/etc).
+
Shared libraries are installed in $PREFIX/lib, which are available from binaries due to Termux setting the $LD_LIBRARY_PATH
+ environment variable. These may clash with Android system binaries in /system/bin, which may force LD_LIBRARY_PATH to be
+ cleared before running system binaries.
+
+
Besides the file system being different, Termux is running as a single-user system without root - each Android app is running as
+ its own Linux user, so running commands inside Termux may not interfere with other installed applications.
+
Running as non-root implies that ports below 1024 cannot be bound to. Many packages have been configured to have compatible
+ default values - the ftpd, httpd, and sshd servers default to 8021, 8080 and 8022, respectively.
+
+
Add-on: API
+
The API add-on exposes Android system functionality such as SMS messages, GPS location or the Text-to-speech functionality through command line tools.
+ * about memory leaks.
+ */
+public final class TermuxActivity extends Activity implements ServiceConnection {
+
+ private static final int CONTEXTMENU_SELECT_ID = 0;
+ private static final int CONTEXTMENU_PASTE_ID = 3;
+ private static final int CONTEXTMENU_KILL_PROCESS_ID = 4;
+ private static final int CONTEXTMENU_RESET_TERMINAL_ID = 5;
+ private static final int CONTEXTMENU_STYLING_ID = 6;
+ private static final int CONTEXTMENU_TOGGLE_FULLSCREEN_ID = 7;
+ private static final int CONTEXTMENU_HELP_ID = 8;
+
+ private static final int MAX_SESSIONS = 8;
+
+ private static final String RELOAD_STYLE_ACTION = "com.termux.app.reload_style";
+
+ /** The main view of the activity showing the terminal. */
+ TerminalView mTerminalView;
+
+ final FullScreenHelper mFullScreenHelper = new FullScreenHelper(this);
+
+ TermuxPreferences mSettings;
+
+ /**
+ * The connection to the {@link TermuxService}. Requested in {@link #onCreate(Bundle)} with a call to
+ * {@link #bindService(Intent, ServiceConnection, int)}, and obtained and stored in
+ * {@link #onServiceConnected(ComponentName, IBinder)}.
+ */
+ TermuxService mTermService;
+
+ /** Initialized in {@link #onServiceConnected(ComponentName, IBinder)}. */
+ ArrayAdapter mListViewAdapter;
+
+ /** The last toast shown, used cancel current toast before showing new in {@link #showToast(String, boolean)}. */
+ Toast mLastToast;
+
+ /**
+ * If between onResume() and onStop(). Note that only one session is in the foreground of the terminal view at the
+ * time, so if the session causing a change is not in the foreground it should probably be treated as background.
+ */
+ boolean mIsVisible;
+
+ private final BroadcastReceiver mBroadcastReceiever = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (mIsVisible) {
+ String whatToReload = intent.getStringExtra(RELOAD_STYLE_ACTION);
+ if (whatToReload == null || "colors".equals(whatToReload)) mTerminalView.checkForColors();
+ if (whatToReload == null || "font".equals(whatToReload)) mTerminalView.checkForTypeface();
+ }
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle bundle) {
+ super.onCreate(bundle);
+
+ // Prevent overdraw:
+ getWindow().getDecorView().setBackground(null);
+
+ setContentView(R.layout.drawer_layout);
+ mTerminalView = (TerminalView) findViewById(R.id.terminal_view);
+ mSettings = new TermuxPreferences(this);
+ mTerminalView.setTextSize(mSettings.getFontSize());
+ mFullScreenHelper.setImmersive(mSettings.isFullScreen());
+ mTerminalView.requestFocus();
+
+ OnKeyListener keyListener = new OnKeyListener() {
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() != KeyEvent.ACTION_DOWN) return false;
+
+ final TerminalSession currentSession = getCurrentTermSession();
+
+ if (keyCode == KeyEvent.KEYCODE_ENTER && !currentSession.isRunning()) {
+ // Return pressed with finished session - remove it.
+ currentSession.finishIfRunning();
+
+ int index = mTermService.removeTermSession(currentSession);
+ mListViewAdapter.notifyDataSetChanged();
+ if (mTermService.getSessions().isEmpty()) {
+ // There are no sessions to show, so finish the activity.
+ finish();
+ } else {
+ if (index >= mTermService.getSessions().size()) {
+ index = mTermService.getSessions().size() - 1;
+ }
+ switchToSession(mTermService.getSessions().get(index));
+ }
+ return true;
+ } else if (!(event.isCtrlPressed() && event.isShiftPressed())) {
+ // Only hook shortcuts with Ctrl+Shift down.
+ return false;
+ }
+
+ // Get the unmodified code point:
+ int unicodeChar = event.getUnicodeChar(0);
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_DOWN || unicodeChar == 'n'/* next */) {
+ int index = mTermService.getSessions().indexOf(currentSession);
+ if (++index >= mTermService.getSessions().size()) index = 0;
+ switchToSession(mTermService.getSessions().get(index));
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_UP || unicodeChar == 'p' /* previous */) {
+ int index = mTermService.getSessions().indexOf(currentSession);
+ if (--index < 0) index = mTermService.getSessions().size() - 1;
+ switchToSession(mTermService.getSessions().get(index));
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+ getDrawer().openDrawer(Gravity.START);
+ } else if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
+ getDrawer().closeDrawers();
+ } else if (unicodeChar == 'f'/* full screen */) {
+ toggleImmersive();
+ } else if (unicodeChar == 'm'/* menu */) {
+ mTerminalView.showContextMenu();
+ } else if (unicodeChar == 'r'/* rename */) {
+ renameSession(currentSession);
+ } else if (unicodeChar == 'c'/* create */) {
+ addNewSession(false, null);
+ } else if (unicodeChar == 'u' /* urls */) {
+ showUrlSelection();
+ } else if (unicodeChar == 'v') {
+ doPaste();
+ } else if (unicodeChar == '+' || event.getUnicodeChar(KeyEvent.META_SHIFT_ON) == '+') {
+ // We also check for the shifted char here since shift may be required to produce '+',
+ // see https://github.com/termux/termux-api/issues/2
+ changeFontSize(true);
+ } else if (unicodeChar == '-') {
+ changeFontSize(false);
+ } else if (unicodeChar >= '1' && unicodeChar <= '9') {
+ int num = unicodeChar - '1';
+ if (mTermService.getSessions().size() > num) switchToSession(mTermService.getSessions().get(num));
+ }
+ return true;
+ }
+ };
+ mTerminalView.setOnKeyListener(keyListener);
+ findViewById(R.id.left_drawer_list).setOnKeyListener(keyListener);
+
+ mTerminalView.setOnKeyListener(new TerminalKeyListener() {
+ @Override
+ public float onScale(float scale) {
+ if (scale < 0.9f || scale > 1.1f) {
+ boolean increase = scale > 1.f;
+ changeFontSize(increase);
+ return 1.0f;
+ }
+ return scale;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent event) {
+ mTerminalView.showContextMenu();
+ }
+
+ @Override
+ public void onSingleTapUp(MotionEvent e) {
+ // Toggle keyboard visibility if tapping with a finger:
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
+ }
+
+ });
+
+ findViewById(R.id.new_session_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ addNewSession(false, null);
+ }
+ });
+
+ findViewById(R.id.new_session_button).setOnLongClickListener(new OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ Resources res = getResources();
+ new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.new_session)
+ .setItems(new String[] { res.getString(R.string.new_session_normal_unnamed), res.getString(R.string.new_session_normal_named),
+ res.getString(R.string.new_session_failsafe) }, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case 0:
+ addNewSession(false, null);
+ break;
+ case 1:
+ DialogUtils.textInput(TermuxActivity.this, R.string.session_new_named_title, R.string.session_new_named_positive_button, null,
+ new DialogUtils.TextSetListener() {
+ @Override
+ public void onTextSet(String text) {
+ addNewSession(false, text);
+ }
+ });
+ break;
+ case 2:
+ addNewSession(true, null);
+ break;
+ }
+ }
+ }).show();
+ return true;
+ }
+ });
+
+ findViewById(R.id.toggle_keyboard_button).setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+ imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0);
+ getDrawer().closeDrawers();
+ }
+ });
+
+ registerForContextMenu(mTerminalView);
+
+ Intent serviceIntent = new Intent(this, TermuxService.class);
+ // Start the service and make it run regardless of who is bound to it:
+ startService(serviceIntent);
+ if (!bindService(serviceIntent, this, 0)) throw new RuntimeException("bindService() failed");
+
+ mTerminalView.checkForTypeface();
+ mTerminalView.checkForColors();
+ }
+
+ /**
+ * Part of the {@link ServiceConnection} interface. The service is bound with
+ * {@link #bindService(Intent, ServiceConnection, int)} in {@link #onCreate(Bundle)} which will cause a call to this
+ * callback method.
+ */
+ @Override
+ public void onServiceConnected(ComponentName componentName, IBinder service) {
+ mTermService = ((TermuxService.LocalBinder) service).service;
+
+ mTermService.mSessionChangeCallback = new SessionChangedCallback() {
+ @Override
+ public void onTextChanged(TerminalSession changedSession) {
+ if (!mIsVisible) return;
+ if (getCurrentTermSession() == changedSession) mTerminalView.onScreenUpdated();
+ }
+
+ @Override
+ public void onTitleChanged(TerminalSession updatedSession) {
+ if (!mIsVisible) return;
+ if (updatedSession != getCurrentTermSession()) {
+ // Only show toast for other sessions than the current one, since the user
+ // probably consciously caused the title change to change in the current session
+ // and don't want an annoying toast for that.
+ showToast(toToastTitle(updatedSession), false);
+ }
+ mListViewAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onSessionFinished(final TerminalSession finishedSession) {
+ if (mTermService.mWantsToStop) {
+ // The service wants to stop as soon as possible.
+ finish();
+ return;
+ }
+ if (mIsVisible && finishedSession != getCurrentTermSession()) {
+ // Show toast for non-current sessions that exit.
+ int indexOfSession = mTermService.getSessions().indexOf(finishedSession);
+ // Verify that session was not removed before we got told about it finishing:
+ if (indexOfSession >= 0) showToast(toToastTitle(finishedSession) + " - exited", true);
+ }
+ mListViewAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onClipboardText(TerminalSession session, String text) {
+ if (!mIsVisible) return;
+ showToast("Clipboard set:\n\"" + text + "\"", true);
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(text)));
+ }
+
+ @Override
+ public void onBell(TerminalSession session) {
+ if (mIsVisible) ((Vibrator) getSystemService(VIBRATOR_SERVICE)).vibrate(50);
+ }
+ };
+
+ ListView listView = (ListView) findViewById(R.id.left_drawer_list);
+ mListViewAdapter = new ArrayAdapter(getApplicationContext(), R.layout.line_in_drawer, mTermService.getSessions()) {
+ final StyleSpan boldSpan = new StyleSpan(Typeface.BOLD);
+ final StyleSpan italicSpan = new StyleSpan(Typeface.ITALIC);
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View row = convertView;
+ if (row == null) {
+ LayoutInflater inflater = getLayoutInflater();
+ row = inflater.inflate(R.layout.line_in_drawer, parent, false);
+ }
+
+ TerminalSession sessionAtRow = getItem(position);
+ boolean sessionRunning = sessionAtRow.isRunning();
+
+ TextView firstLineView = (TextView) row.findViewById(R.id.row_line);
+
+ String name = sessionAtRow.mSessionName;
+ String sessionTitle = sessionAtRow.getTitle();
+
+ String numberPart = "[" + (position + 1) + "] ";
+ String sessionNamePart = (TextUtils.isEmpty(name) ? "" : name);
+ String sessionTitlePart = (TextUtils.isEmpty(sessionTitle) ? "" : ((sessionNamePart.isEmpty() ? "" : "\n") + sessionTitle));
+
+ String text = numberPart + sessionNamePart + sessionTitlePart;
+ SpannableString styledText = new SpannableString(text);
+ styledText.setSpan(boldSpan, 0, numberPart.length() + sessionNamePart.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ styledText.setSpan(italicSpan, numberPart.length() + sessionNamePart.length(), text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ firstLineView.setText(styledText);
+
+ if (sessionRunning) {
+ firstLineView.setPaintFlags(firstLineView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);
+ } else {
+ firstLineView.setPaintFlags(firstLineView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
+ }
+ int color = sessionRunning || sessionAtRow.getExitStatus() == 0 ? Color.BLACK : Color.RED;
+ firstLineView.setTextColor(color);
+ return row;
+ }
+ };
+ listView.setAdapter(mListViewAdapter);
+ listView.setOnItemClickListener(new OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ TerminalSession clickedSession = mListViewAdapter.getItem(position);
+ switchToSession(clickedSession);
+ getDrawer().closeDrawers();
+ }
+ });
+ listView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, final int position, long id) {
+ final TerminalSession selectedSession = mListViewAdapter.getItem(position);
+ renameSession(selectedSession);
+ return true;
+ }
+ });
+
+ if (mTermService.getSessions().isEmpty()) {
+ if (mIsVisible) {
+ TermuxInstaller.setupIfNeeded(TermuxActivity.this, new Runnable() {
+ @Override
+ public void run() {
+ if (TermuxPreferences.isShowWelcomeDialog(TermuxActivity.this)) {
+ new AlertDialog.Builder(TermuxActivity.this).setTitle(R.string.welcome_dialog_title).setMessage(R.string.welcome_dialog_body)
+ .setCancelable(false).setPositiveButton(android.R.string.ok, null)
+ .setNegativeButton(R.string.welcome_dialog_dont_show_again_button, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ TermuxPreferences.disableWelcomeDialog(TermuxActivity.this);
+ dialog.dismiss();
+ }
+ }).show();
+ }
+ addNewSession(false, null);
+ }
+ });
+ } else {
+ // The service connected while not in foreground - just bail out.
+ finish();
+ }
+ } else {
+ switchToSession(getStoredCurrentSessionOrLast());
+ }
+ }
+
+ @SuppressLint("InflateParams")
+ void renameSession(final TerminalSession sessionToRename) {
+ DialogUtils.textInput(this, R.string.session_rename_title, R.string.session_rename_positive_button, sessionToRename.mSessionName,
+ new DialogUtils.TextSetListener() {
+ @Override
+ public void onTextSet(String text) {
+ sessionToRename.mSessionName = text;
+ }
+ });
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (mTermService != null) {
+ // Respect being stopped from the TermuxService notification action.
+ finish();
+ }
+ }
+
+ TerminalSession getCurrentTermSession() {
+ return mTerminalView.getCurrentSession();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mIsVisible = true;
+
+ if (mTermService != null) {
+ // The service has connected, but data may have changed since we were last in the foreground.
+ switchToSession(getStoredCurrentSessionOrLast());
+ mListViewAdapter.notifyDataSetChanged();
+ }
+
+ registerReceiver(mBroadcastReceiever, new IntentFilter(RELOAD_STYLE_ACTION));
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mIsVisible = false;
+ TerminalSession currentSession = getCurrentTermSession();
+ if (currentSession != null) TermuxPreferences.storeCurrentSession(this, currentSession);
+ unregisterReceiver(mBroadcastReceiever);
+ getDrawer().closeDrawers();
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (getDrawer().isDrawerOpen(Gravity.START))
+ getDrawer().closeDrawers();
+ else
+ finish();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (mTermService != null) {
+ // Do not leave service with references to activity.
+ mTermService.mSessionChangeCallback = null;
+ mTermService = null;
+ }
+ unbindService(this);
+ }
+
+ DrawerLayout getDrawer() {
+ return (DrawerLayout) findViewById(R.id.drawer_layout);
+ }
+
+ void addNewSession(boolean failSafe, String sessionName) {
+ if (mTermService.getSessions().size() >= MAX_SESSIONS) {
+ new AlertDialog.Builder(this).setTitle(R.string.max_terminals_reached_title).setMessage(R.string.max_terminals_reached_message)
+ .setPositiveButton(android.R.string.ok, null).show();
+ } else {
+ String executablePath = (failSafe ? "/system/bin/sh" : null);
+ TerminalSession newSession = mTermService.createTermSession(executablePath, null, null, failSafe);
+ if (sessionName != null) {
+ newSession.mSessionName = sessionName;
+ }
+ switchToSession(newSession);
+ getDrawer().closeDrawers();
+ }
+ }
+
+ /** Try switching to session and note about it, but do nothing if already displaying the session. */
+ void switchToSession(TerminalSession session) {
+ if (mTerminalView.attachSession(session)) noteSessionInfo();
+ }
+
+ String toToastTitle(TerminalSession session) {
+ final int indexOfSession = mTermService.getSessions().indexOf(session);
+ StringBuilder toastTitle = new StringBuilder("[" + (indexOfSession + 1) + "]");
+ if (!TextUtils.isEmpty(session.mSessionName)) {
+ toastTitle.append(" ").append(session.mSessionName);
+ }
+ String title = session.getTitle();
+ if (!TextUtils.isEmpty(title)) {
+ // Space to "[${NR}] or newline after session name:
+ toastTitle.append(session.mSessionName == null ? " " : "\n");
+ toastTitle.append(title);
+ }
+ return toastTitle.toString();
+ }
+
+ void noteSessionInfo() {
+ if (!mIsVisible) return;
+ TerminalSession session = getCurrentTermSession();
+ final int indexOfSession = mTermService.getSessions().indexOf(session);
+ showToast(toToastTitle(session), false);
+ mListViewAdapter.notifyDataSetChanged();
+ final ListView lv = ((ListView) findViewById(R.id.left_drawer_list));
+ lv.setItemChecked(indexOfSession, true);
+ lv.smoothScrollToPosition(indexOfSession);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ TerminalSession currentSession = getCurrentTermSession();
+ if (currentSession == null) return;
+
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ menu.add(Menu.NONE, CONTEXTMENU_PASTE_ID, Menu.NONE, R.string.paste_text).setEnabled(clipboard.hasPrimaryClip());
+ menu.add(Menu.NONE, CONTEXTMENU_SELECT_ID, Menu.NONE, R.string.select);
+ menu.add(Menu.NONE, CONTEXTMENU_RESET_TERMINAL_ID, Menu.NONE, R.string.reset_terminal);
+ menu.add(Menu.NONE, CONTEXTMENU_KILL_PROCESS_ID, Menu.NONE, R.string.kill_process).setEnabled(currentSession.isRunning());
+ menu.add(Menu.NONE, CONTEXTMENU_TOGGLE_FULLSCREEN_ID, Menu.NONE, R.string.toggle_fullscreen).setCheckable(true).setChecked(mSettings.isFullScreen());
+ menu.add(Menu.NONE, CONTEXTMENU_STYLING_ID, Menu.NONE, R.string.style_terminal);
+ menu.add(Menu.NONE, CONTEXTMENU_HELP_ID, Menu.NONE, R.string.help);
+ }
+
+ /** Hook system menu to show context menu instead. */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ mTerminalView.showContextMenu();
+ return false;
+ }
+
+ void showUrlSelection() {
+ String text = getCurrentTermSession().getEmulator().getScreen().getTranscriptText();
+ // Pattern for recognizing a URL, based off RFC 3986
+ // http://stackoverflow.com/questions/5713558/detect-and-extract-url-from-a-string
+ final Pattern urlPattern = Pattern.compile(
+ "(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" + "[\\p{Alnum}.,%_=?\\-+()\\[\\]\\*$~@!:/{};']*)",
+ Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);
+ LinkedHashSet urlSet = new LinkedHashSet<>();
+ Matcher matcher = urlPattern.matcher(text);
+ while (matcher.find()) {
+ int matchStart = matcher.start(1);
+ int matchEnd = matcher.end();
+ String url = text.substring(matchStart, matchEnd);
+ urlSet.add(url);
+ }
+
+ if (urlSet.isEmpty()) {
+ new AlertDialog.Builder(this).setMessage(R.string.select_url_no_found).show();
+ return;
+ }
+
+ final CharSequence[] urls = urlSet.toArray(new CharSequence[urlSet.size()]);
+ Collections.reverse(Arrays.asList(urls)); // Latest first.
+
+ // Click to copy url to clipboard:
+ final AlertDialog dialog = new AlertDialog.Builder(TermuxActivity.this).setItems(urls, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface di, int which) {
+ String url = (String) urls[which];
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(new ClipData(null, new String[] { "text/plain" }, new ClipData.Item(url)));
+ Toast.makeText(TermuxActivity.this, R.string.select_url_copied_to_clipboard, Toast.LENGTH_LONG).show();
+ }
+ }).setTitle(R.string.select_url_dialog_title).create();
+
+ // Long press to open URL:
+ dialog.setOnShowListener(new OnShowListener() {
+ @Override
+ public void onShow(DialogInterface di) {
+ ListView lv = dialog.getListView(); // this is a ListView with your "buds" in it
+ lv.setOnItemLongClickListener(new OnItemLongClickListener() {
+ @Override
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ dialog.dismiss();
+ String url = (String) urls[position];
+ startActivity(Intent.createChooser(new Intent(Intent.ACTION_VIEW, Uri.parse(url)), null));
+ return true;
+ }
+ });
+ }
+ });
+
+ dialog.show();
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case CONTEXTMENU_SELECT_ID:
+ CharSequence[] items = new CharSequence[] { getString(R.string.select_text), getString(R.string.select_url),
+ getString(R.string.select_all_and_share) };
+ new AlertDialog.Builder(this).setItems(items, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case 0:
+ mTerminalView.toggleSelectingText();
+ break;
+ case 1:
+ showUrlSelection();
+ break;
+ case 2:
+ TerminalSession session = getCurrentTermSession();
+ if (session != null) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("text/plain");
+ intent.putExtra(Intent.EXTRA_TEXT, session.getEmulator().getScreen().getTranscriptText().trim());
+ intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_transcript_title));
+ startActivity(Intent.createChooser(intent, getString(R.string.share_transcript_chooser_title)));
+ }
+ break;
+ }
+ dialog.dismiss();
+ }
+ }).show();
+ return true;
+ case CONTEXTMENU_PASTE_ID:
+ doPaste();
+ return true;
+ case CONTEXTMENU_KILL_PROCESS_ID:
+ final AlertDialog.Builder b = new AlertDialog.Builder(this);
+ b.setIcon(android.R.drawable.ic_dialog_alert);
+ b.setMessage(R.string.confirm_kill_process);
+ b.setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.dismiss();
+ getCurrentTermSession().finishIfRunning();
+ }
+ });
+ b.setNegativeButton(android.R.string.no, null);
+ b.show();
+ return true;
+ case CONTEXTMENU_RESET_TERMINAL_ID: {
+ TerminalSession session = getCurrentTermSession();
+ if (session != null) {
+ session.reset();
+ showToast(getResources().getString(R.string.reset_toast_notification), true);
+ }
+ return true;
+ }
+ case CONTEXTMENU_STYLING_ID: {
+ Intent stylingIntent = new Intent();
+ stylingIntent.setClassName("com.termux.styling", "com.termux.styling.TermuxStyleActivity");
+ try {
+ startActivity(stylingIntent);
+ } catch (ActivityNotFoundException e) {
+ new AlertDialog.Builder(this).setMessage(R.string.styling_not_installed)
+ .setPositiveButton(R.string.styling_install, new android.content.DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("http://play.google.com/store/apps/details?id=com.termux.styling")));
+ }
+ }).setNegativeButton(android.R.string.cancel, null).show();
+ }
+ }
+ return true;
+ case CONTEXTMENU_TOGGLE_FULLSCREEN_ID:
+ toggleImmersive();
+ return true;
+ case CONTEXTMENU_HELP_ID:
+ startActivity(new Intent(this, TermuxHelpActivity.class));
+ return true;
+ default:
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ void toggleImmersive() {
+ boolean newValue = !mSettings.isFullScreen();
+ mSettings.setFullScreen(this, newValue);
+ mFullScreenHelper.setImmersive(newValue);
+ }
+
+ void changeFontSize(boolean increase) {
+ mSettings.changeFontSize(this, increase);
+ mTerminalView.setTextSize(mSettings.getFontSize());
+ }
+
+ void doPaste() {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboard.getPrimaryClip();
+ if (clipData == null) return;
+ CharSequence paste = clipData.getItemAt(0).coerceToText(this);
+ if (!TextUtils.isEmpty(paste)) getCurrentTermSession().getEmulator().paste(paste.toString());
+ }
+
+ /** The current session as stored or the last one if that does not exist. */
+ public TerminalSession getStoredCurrentSessionOrLast() {
+ TerminalSession stored = TermuxPreferences.getCurrentSession(this);
+ if (stored != null) return stored;
+ int numberOfSessions = mTermService.getSessions().size();
+ if (numberOfSessions == 0) return null;
+ return mTermService.getSessions().get(numberOfSessions - 1);
+ }
+
+ /** Show a toast and dismiss the last one if still visible. */
+ void showToast(String text, boolean longDuration) {
+ if (mLastToast != null) mLastToast.cancel();
+ mLastToast = Toast.makeText(TermuxActivity.this, text, longDuration ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
+ mLastToast.setGravity(Gravity.TOP, 0, 0);
+ mLastToast.show();
+ }
+
+}
diff --git a/app/src/main/java/com/termux/app/TermuxHelpActivity.java b/app/src/main/java/com/termux/app/TermuxHelpActivity.java
new file mode 100644
index 0000000000..a072b443d9
--- /dev/null
+++ b/app/src/main/java/com/termux/app/TermuxHelpActivity.java
@@ -0,0 +1,46 @@
+package com.termux.app;
+
+import android.app.Activity;
+import android.content.ActivityNotFoundException;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.webkit.WebView;
+import android.webkit.WebViewClient;
+
+/** Basic embedded browser for viewing the bundled help page. */
+public final class TermuxHelpActivity extends Activity {
+
+ private WebView mWebView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mWebView = new WebView(this);
+ setContentView(mWebView);
+ mWebView.setWebViewClient(new WebViewClient() {
+ @Override
+ public boolean shouldOverrideUrlLoading(WebView view, String url) {
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ } catch (ActivityNotFoundException e) {
+ // TODO: Android TV does not have a system browser - but needs better method of getting back
+ // than navigating deep here.
+ return false;
+ }
+ return true;
+ }
+ });
+ mWebView.loadUrl("file:///android_asset/help.html");
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mWebView.canGoBack()) {
+ mWebView.goBack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java
new file mode 100644
index 0000000000..714df09e8a
--- /dev/null
+++ b/app/src/main/java/com/termux/app/TermuxInstaller.java
@@ -0,0 +1,193 @@
+package com.termux.app;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.system.Os;
+import android.util.Log;
+import android.util.Pair;
+
+import com.termux.R;
+import com.termux.terminal.EmulatorDebug;
+
+/**
+ * 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.
+ *
+ * (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.
+ *
+ * (4) The architecture is determined and an appropriate bootstrap zip url is determined in {@link #determineZipUrl()}.
+ *
+ * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
+ * continously encountering zip file entries:
+ *
+ * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
+ *
+ * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
+ */
+final class TermuxInstaller {
+
+ /** Performs setup if necessary. */
+ static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
+ // Termux can only be run as the primary user (device owner) since only that
+ // account has the expected file system paths. Verify that:
+ android.os.UserManager um = (android.os.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)
+ .setOnDismissListener(new OnDismissListener() {
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ System.exit(0);
+ }
+ }).setPositiveButton(android.R.string.ok, null).show();
+ return;
+ }
+
+ final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
+ if (PREFIX_FILE.isDirectory()) {
+ whenDone.run();
+ return;
+ }
+
+ final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
+ final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
+
+ if (STAGING_PREFIX_FILE.exists()) {
+ deleteFolder(STAGING_PREFIX_FILE);
+ }
+
+ final byte[] buffer = new byte[8096];
+ final List> symlinks = new ArrayList<>(50);
+
+ final URL zipUrl = determineZipUrl();
+ try (ZipInputStream zipInput = new ZipInputStream(zipUrl.openStream())) {
+ ZipEntry zipEntry;
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ if (zipEntry.getName().equals("SYMLINKS.txt")) {
+ BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
+ String line;
+ while ((line = symlinksReader.readLine()) != null) {
+ String[] parts = line.split("←");
+ if (parts.length != 2) throw new RuntimeException("Malformed symlink line: " + line);
+ String oldPath = parts[0];
+ String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
+ symlinks.add(Pair.create(oldPath, newPath));
+ }
+ } else {
+ String zipEntryName = zipEntry.getName();
+ File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
+ if (zipEntry.isDirectory()) {
+ if (!targetFile.mkdirs()) throw new RuntimeException("Failed to create directory: " + targetFile.getAbsolutePath());
+ } else {
+ try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
+ int readBytes;
+ while ((readBytes = zipInput.read(buffer)) != -1)
+ outStream.write(buffer, 0, readBytes);
+ }
+ if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
+ Os.chmod(targetFile.getAbsolutePath(), 0700);
+ }
+ }
+ }
+ }
+ }
+
+ if (symlinks.isEmpty()) throw new RuntimeException("No SYMLINKS.txt encountered");
+ for (Pair symlink : symlinks) {
+ Os.symlink(symlink.first, symlink.second);
+ }
+
+ if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
+ throw new RuntimeException("Unable to rename staging folder");
+ }
+
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ whenDone.run();
+ }
+ });
+ } catch (final Exception e) {
+ Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
+ .setNegativeButton(R.string.bootstrap_error_abort, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ activity.finish();
+ }
+ }).setPositiveButton(R.string.bootstrap_error_try_again, new OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ TermuxInstaller.setupIfNeeded(activity, whenDone);
+ }
+ }).show();
+ }
+ });
+ } finally {
+ activity.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ progress.dismiss();
+ }
+ });
+ }
+ }
+ }.start();
+ }
+
+ /** Get bootstrap zip url for this systems cpu architecture. */
+ static URL determineZipUrl() throws MalformedURLException {
+ String arch = System.getProperty("os.arch");
+ if (arch.startsWith("arm") || arch.equals("aarch64")) {
+ // Handle different arm variants such as armv7l:
+ arch = "arm";
+ } else if (arch.equals("x86_64")) {
+ arch = "i686";
+ }
+ return new URL("http://apt.termux.com/bootstrap/bootstrap-" + arch + ".zip");
+ }
+
+ /** Delete a folder and all its content or throw. */
+ static void deleteFolder(File fileOrDirectory) {
+ File[] children = fileOrDirectory.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteFolder(child);
+ }
+ }
+ if (!fileOrDirectory.delete()) {
+ throw new RuntimeException("Unable to delete " + (fileOrDirectory.isDirectory() ? "directory " : "file ") + fileOrDirectory.getAbsolutePath());
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/termux/app/TermuxPreferences.java b/app/src/main/java/com/termux/app/TermuxPreferences.java
new file mode 100644
index 0000000000..562b853a1b
--- /dev/null
+++ b/app/src/main/java/com/termux/app/TermuxPreferences.java
@@ -0,0 +1,89 @@
+package com.termux.app;
+
+import com.termux.terminal.TerminalSession;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.TypedValue;
+
+final class TermuxPreferences {
+
+ private final int MIN_FONTSIZE;
+ private static final int MAX_FONTSIZE = 256;
+ private static final String FULLSCREEN_KEY = "fullscreen";
+ private static final String FONTSIZE_KEY = "fontsize";
+ private static final String CURRENT_SESSION_KEY = "current_session";
+ private static final String SHOW_WELCOME_DIALOG_KEY = "intro_dialog";
+
+ private boolean mFullScreen;
+ private int mFontSize;
+
+ TermuxPreferences(Context context) {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+
+ float dipInPixels = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, context.getResources().getDisplayMetrics());
+
+ // This is a bit arbitrary and sub-optimal. We want to give a sensible default for minimum font size
+ // to prevent invisible text due to zoom be mistake:
+ MIN_FONTSIZE = (int) (4f * dipInPixels);
+
+ mFullScreen = prefs.getBoolean(FULLSCREEN_KEY, false);
+
+ // http://www.google.com/design/spec/style/typography.html#typography-line-height
+ int defaultFontSize = Math.round(12 * dipInPixels);
+ // Make it divisible by 2 since that is the minimal adjustment step:
+ if (defaultFontSize % 2 == 1) defaultFontSize--;
+
+ try {
+ mFontSize = Integer.parseInt(prefs.getString(FONTSIZE_KEY, Integer.toString(defaultFontSize)));
+ } catch (NumberFormatException | ClassCastException e) {
+ mFontSize = defaultFontSize;
+ }
+ mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
+ }
+
+ boolean isFullScreen() {
+ return mFullScreen;
+ }
+
+ void setFullScreen(Context context, boolean newValue) {
+ mFullScreen = newValue;
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit().putBoolean(FULLSCREEN_KEY, newValue).apply();
+ }
+
+ int getFontSize() {
+ return mFontSize;
+ }
+
+ void changeFontSize(Context context, boolean increase) {
+ mFontSize += (increase ? 1 : -1) * 2;
+ mFontSize = Math.max(MIN_FONTSIZE, Math.min(mFontSize, MAX_FONTSIZE));
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ prefs.edit().putString(FONTSIZE_KEY, Integer.toString(mFontSize)).apply();
+ }
+
+ static void storeCurrentSession(Context context, TerminalSession session) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putString(TermuxPreferences.CURRENT_SESSION_KEY, session.mHandle).commit();
+ }
+
+ static TerminalSession getCurrentSession(TermuxActivity context) {
+ String sessionHandle = PreferenceManager.getDefaultSharedPreferences(context).getString(TermuxPreferences.CURRENT_SESSION_KEY, "");
+ for (int i = 0, len = context.mTermService.getSessions().size(); i < len; i++) {
+ TerminalSession session = context.mTermService.getSessions().get(i);
+ if (session.mHandle.equals(sessionHandle)) return session;
+ }
+ return null;
+ }
+
+ public static boolean isShowWelcomeDialog(Context context) {
+ return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(SHOW_WELCOME_DIALOG_KEY, true);
+ }
+
+ public static void disableWelcomeDialog(Context context) {
+ PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(SHOW_WELCOME_DIALOG_KEY, false).apply();
+ }
+
+}
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
new file mode 100644
index 0000000000..c6941f4b8f
--- /dev/null
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -0,0 +1,346 @@
+package com.termux.app;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.termux.R;
+import com.termux.terminal.EmulatorDebug;
+import com.termux.terminal.TerminalSession;
+import com.termux.terminal.TerminalSession.SessionChangedCallback;
+
+import android.annotation.SuppressLint;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+
+/**
+ * A service holding a list of terminal sessions, {@link #mTerminalSessions}, showing a foreground notification while
+ * running so that it is not terminated. The user interacts with the session through {@link TermuxActivity}, but this
+ * service may outlive the activity when the user or the system disposes of the activity. In that case the user may
+ * restart {@link TermuxActivity} later to yet again access the sessions.
+ *
+ * In order to keep both terminal sessions and spawned processes (who may outlive the terminal sessions) alive as long
+ * as wanted by the user this service is a foreground service, {@link Service#startForeground(int, Notification)}.
+ *
+ * Optionally may hold a wake and a wifi lock, in which case that is shown in the notification - see
+ * {@link #buildNotification()}.
+ */
+public final class TermuxService extends Service implements SessionChangedCallback {
+
+ /** Note that this is a symlink on the Android M preview. */
+ @SuppressLint("SdCardPath")
+ public static final String FILES_PATH = "/data/data/com.termux/files";
+ public static final String PREFIX_PATH = FILES_PATH + "/usr";
+ public static final String HOME_PATH = FILES_PATH + "/home";
+
+ private static final int NOTIFICATION_ID = 1337;
+
+ /** Intent action to stop the service. */
+ private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
+ /** Intent action to toggle the wake lock, {@link #mWakeLock}, which this service may hold. */
+ private static final String ACTION_LOCK_WAKE = "com.termux.service_toggle_wake_lock";
+ /** Intent action to toggle the wifi lock, {@link #mWifiLock}, which this service may hold. */
+ private static final String ACTION_LOCK_WIFI = "com.termux.service_toggle_wifi_lock";
+ /** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
+ private static final String ACTION_EXECUTE = "com.termux.service_execute";
+
+ /** This service is only bound from inside the same process and never uses IPC. */
+ class LocalBinder extends Binder {
+ public final TermuxService service = TermuxService.this;
+ }
+
+ private final IBinder mBinder = new LocalBinder();
+
+ /**
+ * The terminal sessions which this service manages.
+ *
+ * Note that this list is observed by {@link TermuxActivity#mListViewAdapter}, so any changes must be made on the UI
+ * thread and followed by a call to {@link ArrayAdapter#notifyDataSetChanged()} }.
+ */
+ final List mTerminalSessions = new ArrayList<>();
+
+ /** Note that the service may often outlive the activity, so need to clear this reference. */
+ SessionChangedCallback mSessionChangeCallback;
+
+ private PowerManager.WakeLock mWakeLock;
+ private WifiManager.WifiLock mWifiLock;
+
+ /** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
+ boolean mWantsToStop = false;
+
+ @SuppressLint("Wakelock")
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ String action = intent.getAction();
+ if (ACTION_STOP_SERVICE.equals(action)) {
+ mWantsToStop = true;
+ for (int i = 0; i < mTerminalSessions.size(); i++)
+ mTerminalSessions.get(i).finishIfRunning();
+ stopSelf();
+ } else if (ACTION_LOCK_WAKE.equals(action)) {
+ if (mWakeLock == null) {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, EmulatorDebug.LOG_TAG);
+ mWakeLock.acquire();
+ } else {
+ mWakeLock.release();
+ mWakeLock = null;
+ }
+ updateNotification();
+ } else if (ACTION_LOCK_WIFI.equals(action)) {
+ if (mWifiLock == null) {
+ WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
+ mWifiLock = wm.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, EmulatorDebug.LOG_TAG);
+ mWifiLock.acquire();
+ } else {
+ mWifiLock.release();
+ mWifiLock = null;
+ }
+ updateNotification();
+ } else if (ACTION_EXECUTE.equals(action)) {
+ Uri executableUri = intent.getData();
+ String executablePath = (executableUri == null ? null : executableUri.getPath());
+ String[] arguments = (executableUri == null ? null : intent.getStringArrayExtra("com.termux.execute.arguments"));
+ String cwd = intent.getStringExtra("com.termux.execute.cwd");
+ TerminalSession newSession = createTermSession(executablePath, arguments, cwd, false);
+
+ // 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);
+ name = name.replace('-', ' ');
+ newSession.mSessionName = name;
+ }
+
+ // Make the newly created session the current one to be displayed:
+ TermuxPreferences.storeCurrentSession(this, newSession);
+
+ // Launch the main Termux app, which will now show to current session:
+ startActivity(new Intent(this, TermuxActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ } else if (action != null) {
+ Log.e(EmulatorDebug.LOG_TAG, "Unknown TermuxService action: '" + action + "'");
+ }
+
+ // If this service really do get killed, there is no point restarting it automatically - let the user do on next
+ // start of {@link Term):
+ return Service.START_NOT_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onCreate() {
+ startForeground(NOTIFICATION_ID, buildNotification());
+ }
+
+ /** Update the shown foreground service notification after making any changes that affect it. */
+ private void updateNotification() {
+ if (mWakeLock == null && mWifiLock == null && getSessions().isEmpty()) {
+ // Exit if we are updating after the user disabled all locks with no sessions.
+ stopSelf();
+ } else {
+ ((NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, buildNotification());
+ }
+ }
+
+ private Notification buildNotification() {
+ Intent notifyIntent = new Intent(this, TermuxActivity.class);
+ // PendingIntent#getActivity(): "Note that the activity will be started outside of the context of an existing
+ // activity, so you must use the Intent.FLAG_ACTIVITY_NEW_TASK launch flag in the Intent":
+ notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0);
+
+ int sessionCount = mTerminalSessions.size();
+ String contentText = sessionCount + " terminal session" + (sessionCount == 1 ? "" : "s");
+
+ boolean wakeLockHeld = mWakeLock != null;
+ boolean wifiLockHeld = mWifiLock != null;
+ if (wakeLockHeld && wifiLockHeld) {
+ contentText += " (wake&wifi lock held)";
+ } else if (wakeLockHeld) {
+ contentText += " (wake lock held)";
+ } else if (wifiLockHeld) {
+ contentText += " (wifi lock held)";
+ }
+
+ Notification.Builder builder = new Notification.Builder(this);
+ builder.setContentTitle(getText(R.string.application_name));
+ builder.setContentText(contentText);
+ builder.setSmallIcon(R.drawable.ic_service_notification);
+ builder.setContentIntent(pendingIntent);
+ builder.setOngoing(true);
+
+ // If holding a wake or wifi lock consider the notification of high priority since it's using power,
+ // otherwise use a minimal priority since this is just a background service notification:
+ builder.setPriority((wakeLockHeld || wifiLockHeld) ? Notification.PRIORITY_HIGH : Notification.PRIORITY_MIN);
+
+ // No need to show a timestamp:
+ builder.setShowWhen(false);
+
+ // Background color for small notification icon:
+ builder.setColor(0xFF000000);
+
+ Resources res = getResources();
+ Intent exitIntent = new Intent(this, TermuxService.class).setAction(ACTION_STOP_SERVICE);
+ builder.addAction(android.R.drawable.ic_delete, res.getString(R.string.notification_action_exit), PendingIntent.getService(this, 0, exitIntent, 0));
+
+ Intent toggleWakeLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WAKE);
+ builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wakelock),
+ PendingIntent.getService(this, 0, toggleWakeLockIntent, 0));
+
+ Intent toggleWifiLockIntent = new Intent(this, TermuxService.class).setAction(ACTION_LOCK_WIFI);
+ builder.addAction(android.R.drawable.ic_lock_lock, res.getString(R.string.notification_action_wifilock),
+ PendingIntent.getService(this, 0, toggleWifiLockIntent, 0));
+
+ return builder.build();
+ }
+
+ @Override
+ public void onDestroy() {
+ if (mWakeLock != null) mWakeLock.release();
+ if (mWifiLock != null) mWifiLock.release();
+
+ stopForeground(true);
+
+ for (int i = 0; i < mTerminalSessions.size(); i++)
+ mTerminalSessions.get(i).finishIfRunning();
+ mTerminalSessions.clear();
+ }
+
+ public List getSessions() {
+ return mTerminalSessions;
+ }
+
+ TerminalSession createTermSession(String executablePath, String[] arguments, String cwd, boolean failSafe) {
+ new File(HOME_PATH).mkdirs();
+
+ if (cwd == null) cwd = HOME_PATH;
+
+ final String termEnv = "TERM=xterm-256color";
+ final String homeEnv = "HOME=" + HOME_PATH;
+ final String prefixEnv = "PREFIX=" + PREFIX_PATH;
+ final String androidRootEnv = "ANDROID_ROOT=" + System.getenv("ANDROID_ROOT");
+ final String androidDataEnv = "ANDROID_DATA=" + System.getenv("ANDROID_DATA");
+ String[] env;
+ if (failSafe) {
+ env = new String[] { termEnv, homeEnv, prefixEnv, androidRootEnv, androidDataEnv };
+ } else {
+ final String ps1Env = "PS1=$ ";
+ final String ldEnv = "LD_LIBRARY_PATH=" + PREFIX_PATH + "/lib";
+ final String langEnv = "LANG=en_US.UTF-8";
+ final String pathEnv = "PATH=" + PREFIX_PATH + "/bin:" + PREFIX_PATH + "/bin/applets:" + System.getenv("PATH");
+ final String pwdEnv = "PWD=" + cwd;
+
+ env = new String[] { termEnv, homeEnv, prefixEnv, ps1Env, ldEnv, langEnv, pathEnv, pwdEnv, androidRootEnv, androidDataEnv };
+ }
+
+ String shellName;
+ if (executablePath == null) {
+ File shell = new File(HOME_PATH, ".termux/shell");
+ if (shell.exists()) {
+ try {
+ File canonicalFile = shell.getCanonicalFile();
+ if (canonicalFile.isFile() && canonicalFile.canExecute()) {
+ executablePath = canonicalFile.getName().equals("busybox") ? (PREFIX_PATH + "/bin/ash") : canonicalFile.getAbsolutePath();
+ } else {
+ Log.w(EmulatorDebug.LOG_TAG, "$HOME/.termux/shell points to non-executable shell: " + canonicalFile.getAbsolutePath());
+ }
+ } catch (IOException e) {
+ Log.e(EmulatorDebug.LOG_TAG, "Error checking $HOME/.termux/shell", e);
+ }
+ }
+
+ if (executablePath == null) {
+ // Try bash, zsh and ash in that order:
+ for (String shellBinary : new String[] { "bash", "zsh", "ash" }) {
+ File shellFile = new File(PREFIX_PATH + "/bin/" + shellBinary);
+ if (shellFile.canExecute()) {
+ executablePath = shellFile.getAbsolutePath();
+ break;
+ }
+ }
+ }
+
+ if (executablePath == null) {
+ // Fall back to system shell as last resort:
+ executablePath = "/system/bin/sh";
+ }
+
+ String[] parts = executablePath.split("/");
+ shellName = "-" + parts[parts.length - 1];
+ } else {
+ int lastSlashIndex = executablePath.lastIndexOf('/');
+ shellName = lastSlashIndex == -1 ? executablePath : executablePath.substring(lastSlashIndex + 1);
+ }
+
+ String[] args;
+ if (arguments == null) {
+ args = new String[] { shellName };
+ } else {
+ args = new String[arguments.length + 1];
+ args[0] = shellName;
+
+ System.arraycopy(arguments, 0, args, 1, arguments.length);
+ }
+
+ TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
+ mTerminalSessions.add(session);
+ updateNotification();
+ return session;
+ }
+
+ public int removeTermSession(TerminalSession sessionToRemove) {
+ int indexOfRemoved = mTerminalSessions.indexOf(sessionToRemove);
+ mTerminalSessions.remove(indexOfRemoved);
+ if (mTerminalSessions.isEmpty() && mWakeLock == null) {
+ // Finish if there are no sessions left and the wake lock is not held, otherwise keep the service alive if
+ // holding wake lock since there may be daemon processes (e.g. sshd) running.
+ stopSelf();
+ } else {
+ updateNotification();
+ }
+ return indexOfRemoved;
+ }
+
+ @Override
+ public void onTitleChanged(TerminalSession changedSession) {
+ if (mSessionChangeCallback != null) mSessionChangeCallback.onTitleChanged(changedSession);
+ }
+
+ @Override
+ public void onSessionFinished(final TerminalSession finishedSession) {
+ if (mSessionChangeCallback != null) mSessionChangeCallback.onSessionFinished(finishedSession);
+ }
+
+ @Override
+ public void onTextChanged(TerminalSession changedSession) {
+ if (mSessionChangeCallback != null) mSessionChangeCallback.onTextChanged(changedSession);
+ }
+
+ @Override
+ public void onClipboardText(TerminalSession session, String text) {
+ if (mSessionChangeCallback != null) mSessionChangeCallback.onClipboardText(session, text);
+ }
+
+ @Override
+ public void onBell(TerminalSession session) {
+ if (mSessionChangeCallback != null) mSessionChangeCallback.onBell(session);
+ }
+
+}
diff --git a/app/src/main/java/com/termux/drawer/DrawerLayout.java b/app/src/main/java/com/termux/drawer/DrawerLayout.java
new file mode 100644
index 0000000000..9523c917c3
--- /dev/null
+++ b/app/src/main/java/com/termux/drawer/DrawerLayout.java
@@ -0,0 +1,1799 @@
+package com.termux.drawer;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.List;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/**
+ * DrawerLayout acts as a top-level container for window content that allows for interactive "drawer" views to be pulled
+ * out from the edge of the window.
+ *
+ *
+ * Drawer positioning and layout is controlled using the android:layout_gravity attribute on child views
+ * corresponding to which side of the view you want the drawer to emerge from: left or right. (Or start/end on platform
+ * versions that support layout direction.)
+ *
+ *
+ *
+ * To use a DrawerLayout, position your primary content view as the first child with a width and height of
+ * match_parent. Add drawers as child views after the main content view and set the
+ * layout_gravity appropriately. Drawers commonly use match_parent for height with a fixed
+ * width.
+ *
+ *
+ *
+ * {@link DrawerListener} can be used to monitor the state and motion of drawer views. Avoid performing expensive
+ * operations such as layout during animation as it can cause stuttering; try to perform expensive operations during the
+ * {@link #STATE_IDLE} state. {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.
+ *
+ *
+ *
+ * As per the Android Design guide, any drawers
+ * positioned to the left/start should always contain content for navigating around the application, whereas any drawers
+ * positioned to the right/end should always contain actions to take on the current content. This preserves the same
+ * navigation left, actions right structure present in the Action Bar and elsewhere.
+ *
+ */
+@SuppressLint("RtlHardcoded")
+public class DrawerLayout extends ViewGroup {
+ private static final String TAG = "DrawerLayout";
+
+ /**
+ * Indicates that any drawers are in an idle, settled state. No animation is in progress.
+ */
+ public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
+
+ /**
+ * Indicates that a drawer is currently being dragged by the user.
+ */
+ public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
+
+ /**
+ * Indicates that a drawer is in the process of settling to a final position.
+ */
+ public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
+
+ /**
+ * The drawer is unlocked.
+ */
+ public static final int LOCK_MODE_UNLOCKED = 0;
+
+ /**
+ * The drawer is locked closed. The user may not open it, though the app may open it programmatically.
+ */
+ public static final int LOCK_MODE_LOCKED_CLOSED = 1;
+
+ /**
+ * The drawer is locked open. The user may not close it, though the app may close it programmatically.
+ */
+ public static final int LOCK_MODE_LOCKED_OPEN = 2;
+
+ private static final int MIN_DRAWER_MARGIN = 64; // dp
+
+ private static final int DEFAULT_SCRIM_COLOR = 0x99000000;
+
+ /**
+ * Length of time to delay before peeking the drawer.
+ */
+ private static final int PEEK_DELAY = 160; // ms
+
+ /**
+ * Minimum velocity that will be detected as a fling
+ */
+ private static final int MIN_FLING_VELOCITY = 400; // dips per second
+
+ /**
+ * Experimental feature.
+ */
+ private static final boolean ALLOW_EDGE_LOCK = false;
+
+ private static final boolean CHILDREN_DISALLOW_INTERCEPT = true;
+
+ private static final float TOUCH_SLOP_SENSITIVITY = 1.f;
+
+ static final int[] LAYOUT_ATTRS = new int[] { android.R.attr.layout_gravity };
+
+ /** Whether we can use NO_HIDE_DESCENDANTS accessibility importance. */
+ static final boolean CAN_HIDE_DESCENDANTS = Build.VERSION.SDK_INT >= 19;
+
+ private final ChildAccessibilityDelegate mChildAccessibilityDelegate = new ChildAccessibilityDelegate();
+
+ private int mMinDrawerMargin;
+
+ private int mScrimColor = DEFAULT_SCRIM_COLOR;
+ private float mScrimOpacity;
+ private Paint mScrimPaint = new Paint();
+
+ private final ViewDragHelper mLeftDragger;
+ private final ViewDragHelper mRightDragger;
+ private final ViewDragCallback mLeftCallback;
+ private final ViewDragCallback mRightCallback;
+ private int mDrawerState;
+ private boolean mInLayout;
+ private boolean mFirstLayout = true;
+ private int mLockModeLeft;
+ private int mLockModeRight;
+ private boolean mChildrenCanceledTouch;
+
+ private DrawerListener mListener;
+
+ private float mInitialMotionX;
+ private float mInitialMotionY;
+
+ private Drawable mShadowLeft;
+ private Drawable mShadowRight;
+ private Drawable mStatusBarBackground;
+
+ private CharSequence mTitleLeft;
+ private CharSequence mTitleRight;
+
+ private Object mLastInsets;
+ private boolean mDrawStatusBarBackground;
+
+ /**
+ * Listener for monitoring events about drawers.
+ */
+ public interface DrawerListener {
+ /**
+ * Called when a drawer's position changes.
+ *
+ * @param drawerView
+ * The child view that was moved
+ * @param slideOffset
+ * The new offset of this drawer within its range, from 0-1
+ */
+ void onDrawerSlide(View drawerView, float slideOffset);
+
+ /**
+ * Called when a drawer has settled in a completely open state. The drawer is interactive at this point.
+ *
+ * @param drawerView
+ * Drawer view that is now open
+ */
+ void onDrawerOpened(View drawerView);
+
+ /**
+ * Called when a drawer has settled in a completely closed state.
+ *
+ * @param drawerView
+ * Drawer view that is now closed
+ */
+ void onDrawerClosed(View drawerView);
+
+ /**
+ * Called when the drawer motion state changes. The new state will be one of {@link #STATE_IDLE},
+ * {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
+ *
+ * @param newState
+ * The new drawer motion state
+ */
+ void onDrawerStateChanged(int newState);
+ }
+
+ /**
+ * Stub/no-op implementations of all methods of {@link DrawerListener}. Override this if you only care about a few
+ * of the available callback methods.
+ */
+ public static abstract class SimpleDrawerListener implements DrawerListener {
+ @Override
+ public void onDrawerSlide(View drawerView, float slideOffset) {
+ // Empty.
+ }
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ // Empty.
+ }
+
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ // Empty.
+ }
+
+ @Override
+ public void onDrawerStateChanged(int newState) {
+ // Empty.
+ }
+ }
+
+ interface DrawerLayoutCompatImpl {
+ void configureApplyInsets(View drawerLayout);
+
+ void dispatchChildInsets(View child, Object insets, int drawerGravity);
+
+ void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity);
+
+ int getTopInset(Object lastInsets);
+
+ Drawable getDefaultStatusBarBackground(Context context);
+ }
+
+ static class DrawerLayoutCompatImplBase implements DrawerLayoutCompatImpl {
+ @Override
+ public void configureApplyInsets(View drawerLayout) {
+ // This space for rent
+ }
+
+ @Override
+ public void dispatchChildInsets(View child, Object insets, int drawerGravity) {
+ // This space for rent
+ }
+
+ @Override
+ public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) {
+ // This space for rent
+ }
+
+ @Override
+ public int getTopInset(Object insets) {
+ return 0;
+ }
+
+ @Override
+ public Drawable getDefaultStatusBarBackground(Context context) {
+ return null;
+ }
+ }
+
+ public DrawerLayout(Context context) {
+ this(context, null);
+ }
+
+ public DrawerLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DrawerLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+ final float density = getResources().getDisplayMetrics().density;
+ mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f);
+ final float minVel = MIN_FLING_VELOCITY * density;
+
+ mLeftCallback = new ViewDragCallback(Gravity.LEFT);
+ mRightCallback = new ViewDragCallback(Gravity.RIGHT);
+
+ mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback);
+ mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);
+ mLeftDragger.setMinVelocity(minVel);
+ mLeftCallback.setDragger(mLeftDragger);
+
+ mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback);
+ mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT);
+ mRightDragger.setMinVelocity(minVel);
+ mRightCallback.setDragger(mRightDragger);
+
+ // So that we can catch the back button
+ setFocusableInTouchMode(true);
+
+ this.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+
+ this.setAccessibilityDelegate(new AccessibilityDelegate());
+ this.setMotionEventSplittingEnabled(false);
+ if (this.getFitsSystemWindows()) {
+ DrawerLayoutCompatApi21.configureApplyInsets(this);
+ mStatusBarBackground = DrawerLayoutCompatApi21.getDefaultStatusBarBackground(context);
+ }
+ }
+
+ /**
+ * Internal use only; called to apply window insets when configured with fitsSystemWindows="true"
+ */
+ public void setChildInsets(Object insets, boolean draw) {
+ mLastInsets = insets;
+ mDrawStatusBarBackground = draw;
+ setWillNotDraw(!draw && getBackground() == null);
+ requestLayout();
+ }
+
+ /**
+ * Set a simple drawable used for the left or right shadow. The drawable provided must have a nonzero intrinsic
+ * width.
+ *
+ * @param shadowDrawable
+ * Shadow drawable to use at the edge of a drawer
+ * @param gravity
+ * Which drawer the shadow should apply to
+ */
+ public void setDrawerShadow(Drawable shadowDrawable, int gravity) {
+ /*
+ * TODO Someone someday might want to set more complex drawables here. They're probably nuts, but we might want
+ * to consider registering callbacks, setting states, etc. properly.
+ */
+
+ final int absGravity = Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection());
+ if ((absGravity & Gravity.LEFT) == Gravity.LEFT) {
+ mShadowLeft = shadowDrawable;
+ invalidate();
+ }
+ if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) {
+ mShadowRight = shadowDrawable;
+ invalidate();
+ }
+ }
+
+ /**
+ * Set a simple drawable used for the left or right shadow. The drawable provided must have a nonzero intrinsic
+ * width.
+ *
+ * @param resId
+ * Resource id of a shadow drawable to use at the edge of a drawer
+ * @param gravity
+ * Which drawer the shadow should apply to
+ */
+ public void setDrawerShadow(int resId, int gravity) {
+ setDrawerShadow(getResources().getDrawable(resId), gravity);
+ }
+
+ /**
+ * Set a color to use for the scrim that obscures primary content while a drawer is open.
+ *
+ * @param color
+ * Color to use in 0xAARRGGBB format.
+ */
+ public void setScrimColor(int color) {
+ mScrimColor = color;
+ invalidate();
+ }
+
+ /**
+ * Set a listener to be notified of drawer events.
+ *
+ * @param listener
+ * Listener to notify when drawer events occur
+ * @see DrawerListener
+ */
+ public void setDrawerListener(DrawerListener listener) {
+ mListener = listener;
+ }
+
+ /**
+ * Enable or disable interaction with all drawers.
+ *
+ *
+ * This allows the application to restrict the user's ability to open or close any drawer within this layout.
+ * DrawerLayout will still respond to calls to {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a
+ * drawer is locked.
+ *
+ *
+ *
+ * Locking drawers open or closed will implicitly open or close any drawers as appropriate.
+ *
+ *
+ * @param lockMode
+ * The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
+ * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
+ */
+ public void setDrawerLockMode(int lockMode) {
+ setDrawerLockMode(lockMode, Gravity.LEFT);
+ setDrawerLockMode(lockMode, Gravity.RIGHT);
+ }
+
+ /**
+ * Enable or disable interaction with the given drawer.
+ *
+ *
+ * This allows the application to restrict the user's ability to open or close the given drawer. DrawerLayout will
+ * still respond to calls to {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.
+ *
+ *
+ *
+ * Locking a drawer open or closed will implicitly open or close that drawer as appropriate.
+ *
+ *
+ * @param lockMode
+ * The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
+ * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
+ * @param edgeGravity
+ * Gravity.LEFT, RIGHT, START or END. Expresses which drawer to change the mode for.
+ *
+ * @see #LOCK_MODE_UNLOCKED
+ * @see #LOCK_MODE_LOCKED_CLOSED
+ * @see #LOCK_MODE_LOCKED_OPEN
+ */
+ public void setDrawerLockMode(int lockMode, int edgeGravity) {
+ final int absGravity = Gravity.getAbsoluteGravity(edgeGravity, this.getLayoutDirection());
+ if (absGravity == Gravity.LEFT) {
+ mLockModeLeft = lockMode;
+ } else if (absGravity == Gravity.RIGHT) {
+ mLockModeRight = lockMode;
+ }
+ if (lockMode != LOCK_MODE_UNLOCKED) {
+ // Cancel interaction in progress
+ final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger;
+ helper.cancel();
+ }
+ switch (lockMode) {
+ case LOCK_MODE_LOCKED_OPEN:
+ final View toOpen = findDrawerWithGravity(absGravity);
+ if (toOpen != null) {
+ openDrawer(toOpen);
+ }
+ break;
+ case LOCK_MODE_LOCKED_CLOSED:
+ final View toClose = findDrawerWithGravity(absGravity);
+ if (toClose != null) {
+ closeDrawer(toClose);
+ }
+ break;
+ // default: do nothing
+ }
+ }
+
+ /**
+ * Enable or disable interaction with the given drawer.
+ *
+ *
+ * This allows the application to restrict the user's ability to open or close the given drawer. DrawerLayout will
+ * still respond to calls to {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.
+ *
+ *
+ *
+ * Locking a drawer open or closed will implicitly open or close that drawer as appropriate.
+ *
+ *
+ * @param lockMode
+ * The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED},
+ * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
+ * @param drawerView
+ * The drawer view to change the lock mode for
+ *
+ * @see #LOCK_MODE_UNLOCKED
+ * @see #LOCK_MODE_LOCKED_CLOSED
+ * @see #LOCK_MODE_LOCKED_OPEN
+ */
+ public void setDrawerLockMode(int lockMode, View drawerView) {
+ if (!isDrawerView(drawerView)) {
+ throw new IllegalArgumentException("View " + drawerView + " is not a " + "drawer with appropriate layout_gravity");
+ }
+ final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
+ setDrawerLockMode(lockMode, gravity);
+ }
+
+ /**
+ * Check the lock mode of the drawer with the given gravity.
+ *
+ * @param edgeGravity
+ * Gravity of the drawer to check
+ * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
+ */
+ public int getDrawerLockMode(int edgeGravity) {
+ final int absGravity = Gravity.getAbsoluteGravity(edgeGravity, this.getLayoutDirection());
+ if (absGravity == Gravity.LEFT) {
+ return mLockModeLeft;
+ } else if (absGravity == Gravity.RIGHT) {
+ return mLockModeRight;
+ }
+ return LOCK_MODE_UNLOCKED;
+ }
+
+ /**
+ * Check the lock mode of the given drawer view.
+ *
+ * @param drawerView
+ * Drawer view to check lock mode
+ * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}.
+ */
+ public int getDrawerLockMode(View drawerView) {
+ final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
+ if (absGravity == Gravity.LEFT) {
+ return mLockModeLeft;
+ } else if (absGravity == Gravity.RIGHT) {
+ return mLockModeRight;
+ }
+ return LOCK_MODE_UNLOCKED;
+ }
+
+ /**
+ * Sets the title of the drawer with the given gravity.
+ *
+ * When accessibility is turned on, this is the title that will be used to identify the drawer to the active
+ * accessibility service.
+ *
+ * @param edgeGravity
+ * Gravity.LEFT, RIGHT, START or END. Expresses which drawer to set the title for.
+ * @param title
+ * The title for the drawer.
+ */
+ public void setDrawerTitle(int edgeGravity, CharSequence title) {
+ final int absGravity = Gravity.getAbsoluteGravity(edgeGravity, this.getLayoutDirection());
+ if (absGravity == Gravity.LEFT) {
+ mTitleLeft = title;
+ } else if (absGravity == Gravity.RIGHT) {
+ mTitleRight = title;
+ }
+ }
+
+ /**
+ * Returns the title of the drawer with the given gravity.
+ *
+ * @param edgeGravity
+ * Gravity.LEFT, RIGHT, START or END. Expresses which drawer to return the title for.
+ * @return The title of the drawer, or null if none set.
+ * @see #setDrawerTitle(int, CharSequence)
+ */
+ public CharSequence getDrawerTitle(int edgeGravity) {
+ final int absGravity = Gravity.getAbsoluteGravity(edgeGravity, this.getLayoutDirection());
+ if (absGravity == Gravity.LEFT) {
+ return mTitleLeft;
+ } else if (absGravity == Gravity.RIGHT) {
+ return mTitleRight;
+ }
+ return null;
+ }
+
+ /**
+ * Resolve the shared state of all drawers from the component ViewDragHelpers. Should be called whenever a
+ * ViewDragHelper's state changes.
+ */
+ void updateDrawerState(int activeState, View activeDrawer) {
+ final int leftState = mLeftDragger.getViewDragState();
+ final int rightState = mRightDragger.getViewDragState();
+
+ final int state;
+ if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) {
+ state = STATE_DRAGGING;
+ } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) {
+ state = STATE_SETTLING;
+ } else {
+ state = STATE_IDLE;
+ }
+
+ if (activeDrawer != null && activeState == STATE_IDLE) {
+ final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams();
+ if (lp.onScreen == 0) {
+ dispatchOnDrawerClosed(activeDrawer);
+ } else if (lp.onScreen == 1) {
+ dispatchOnDrawerOpened(activeDrawer);
+ }
+ }
+
+ if (state != mDrawerState) {
+ mDrawerState = state;
+
+ if (mListener != null) {
+ mListener.onDrawerStateChanged(state);
+ }
+ }
+ }
+
+ void dispatchOnDrawerClosed(View drawerView) {
+ final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
+ if (lp.knownOpen) {
+ lp.knownOpen = false;
+ if (mListener != null) {
+ mListener.onDrawerClosed(drawerView);
+ }
+
+ updateChildrenImportantForAccessibility(drawerView, false);
+
+ // Only send WINDOW_STATE_CHANGE if the host has window focus. This
+ // may change if support for multiple foreground windows (e.g. IME)
+ // improves.
+ if (hasWindowFocus()) {
+ final View rootView = getRootView();
+ if (rootView != null) {
+ rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+ }
+ }
+ }
+
+ void dispatchOnDrawerOpened(View drawerView) {
+ final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
+ if (!lp.knownOpen) {
+ lp.knownOpen = true;
+ if (mListener != null) {
+ mListener.onDrawerOpened(drawerView);
+ }
+
+ updateChildrenImportantForAccessibility(drawerView, true);
+
+ // Only send WINDOW_STATE_CHANGE if the host has window focus.
+ if (hasWindowFocus()) {
+ sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ }
+
+ drawerView.requestFocus();
+ }
+ }
+
+ private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (!isDrawerOpen && !isDrawerView(child) || isDrawerOpen && child == drawerView) {
+ // Drawer is closed and this is a content view or this is an
+ // open drawer view, so it should be visible.
+ child.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ } else {
+ child.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ }
+ }
+ }
+
+ void dispatchOnDrawerSlide(View drawerView, float slideOffset) {
+ if (mListener != null) {
+ mListener.onDrawerSlide(drawerView, slideOffset);
+ }
+ }
+
+ void setDrawerViewOffset(View drawerView, float slideOffset) {
+ final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
+ if (slideOffset == lp.onScreen) {
+ return;
+ }
+
+ lp.onScreen = slideOffset;
+ dispatchOnDrawerSlide(drawerView, slideOffset);
+ }
+
+ float getDrawerViewOffset(View drawerView) {
+ return ((LayoutParams) drawerView.getLayoutParams()).onScreen;
+ }
+
+ /**
+ * @return the absolute gravity of the child drawerView, resolved according to the current layout direction
+ */
+ int getDrawerViewAbsoluteGravity(View drawerView) {
+ final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity;
+ return Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection());
+ }
+
+ boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) {
+ final int absGravity = getDrawerViewAbsoluteGravity(drawerView);
+ return (absGravity & checkFor) == checkFor;
+ }
+
+ View findOpenDrawer() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (((LayoutParams) child.getLayoutParams()).knownOpen) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ void moveDrawerToOffset(View drawerView, float slideOffset) {
+ final float oldOffset = getDrawerViewOffset(drawerView);
+ final int width = drawerView.getWidth();
+ final int oldPos = (int) (width * oldOffset);
+ final int newPos = (int) (width * slideOffset);
+ final int dx = newPos - oldPos;
+
+ drawerView.offsetLeftAndRight(checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx);
+ setDrawerViewOffset(drawerView, slideOffset);
+ }
+
+ /**
+ * @param gravity
+ * the gravity of the child to return. If specified as a relative value, it will be resolved according to
+ * the current layout direction.
+ * @return the drawer with the specified gravity
+ */
+ View findDrawerWithGravity(int gravity) {
+ final int absHorizGravity = Gravity.getAbsoluteGravity(gravity, this.getLayoutDirection()) & Gravity.HORIZONTAL_GRAVITY_MASK;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final int childAbsGravity = getDrawerViewAbsoluteGravity(child);
+ if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Simple gravity to string - only supports LEFT and RIGHT for debugging output.
+ *
+ * @param gravity
+ * Absolute gravity value
+ * @return LEFT or RIGHT as appropriate, or a hex string
+ */
+ static String gravityToString(int gravity) {
+ if ((gravity & Gravity.LEFT) == Gravity.LEFT) {
+ return "LEFT";
+ }
+ if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) {
+ return "RIGHT";
+ }
+ return Integer.toHexString(gravity);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mFirstLayout = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+ int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) {
+ if (isInEditMode()) {
+ // Don't crash the layout editor. Consume all of the space if specified
+ // or pick a magic number from thin air otherwise.
+ // TODO Better communication with tools of this bogus state.
+ // It will crash on a real device.
+ if (widthMode == MeasureSpec.AT_MOST) {
+ widthMode = MeasureSpec.EXACTLY;
+ } else if (widthMode == MeasureSpec.UNSPECIFIED) {
+ widthMode = MeasureSpec.EXACTLY;
+ widthSize = 300;
+ }
+ if (heightMode == MeasureSpec.AT_MOST) {
+ heightMode = MeasureSpec.EXACTLY;
+ } else if (heightMode == MeasureSpec.UNSPECIFIED) {
+ heightMode = MeasureSpec.EXACTLY;
+ heightSize = 300;
+ }
+ } else {
+ throw new IllegalArgumentException("DrawerLayout must be measured with MeasureSpec.EXACTLY.");
+ }
+ }
+
+ setMeasuredDimension(widthSize, heightSize);
+
+ final boolean applyInsets = mLastInsets != null && this.getFitsSystemWindows();
+ final int layoutDirection = this.getLayoutDirection();
+
+ // Gravity value for each drawer we've seen. Only one of each permitted.
+ int foundDrawers = 0;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (applyInsets) {
+ final int cgrav = Gravity.getAbsoluteGravity(lp.gravity, layoutDirection);
+ if (child.getFitsSystemWindows()) {
+ DrawerLayoutCompatApi21.dispatchChildInsets(child, mLastInsets, cgrav);
+ } else {
+ DrawerLayoutCompatApi21.applyMarginInsets(lp, mLastInsets, cgrav);
+ }
+ }
+
+ if (isContentView(child)) {
+ // Content views get measured at exactly the layout's size.
+ final int contentWidthSpec = MeasureSpec.makeMeasureSpec(widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY);
+ final int contentHeightSpec = MeasureSpec.makeMeasureSpec(heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY);
+ child.measure(contentWidthSpec, contentHeightSpec);
+ } else if (isDrawerView(child)) {
+ final int childGravity = getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK;
+ if ((foundDrawers & childGravity) != 0) {
+ throw new IllegalStateException("Child drawer has absolute gravity " + gravityToString(childGravity) + " but this " + TAG
+ + " already has a " + "drawer view along that edge");
+ }
+ final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, mMinDrawerMargin + lp.leftMargin + lp.rightMargin, lp.width);
+ final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, lp.topMargin + lp.bottomMargin, lp.height);
+ child.measure(drawerWidthSpec, drawerHeightSpec);
+ } else {
+ throw new IllegalStateException("Child " + child + " at index " + i + " does not have a valid layout_gravity - must be Gravity.LEFT, "
+ + "Gravity.RIGHT or Gravity.NO_GRAVITY");
+ }
+ }
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mInLayout = true;
+ final int width = r - l;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (isContentView(child)) {
+ child.layout(lp.leftMargin, lp.topMargin, lp.leftMargin + child.getMeasuredWidth(), lp.topMargin + child.getMeasuredHeight());
+ } else { // Drawer, if it wasn't onMeasure would have thrown an exception.
+ final int childWidth = child.getMeasuredWidth();
+ final int childHeight = child.getMeasuredHeight();
+ int childLeft;
+
+ final float newOffset;
+ if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
+ childLeft = -childWidth + (int) (childWidth * lp.onScreen);
+ newOffset = (float) (childWidth + childLeft) / childWidth;
+ } else { // Right; onMeasure checked for us.
+ childLeft = width - (int) (childWidth * lp.onScreen);
+ newOffset = (float) (width - childLeft) / childWidth;
+ }
+
+ final boolean changeOffset = newOffset != lp.onScreen;
+
+ final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (vgrav) {
+ default:
+ case Gravity.TOP: {
+ child.layout(childLeft, lp.topMargin, childLeft + childWidth, lp.topMargin + childHeight);
+ break;
+ }
+
+ case Gravity.BOTTOM: {
+ final int height = b - t;
+ child.layout(childLeft, height - lp.bottomMargin - child.getMeasuredHeight(), childLeft + childWidth, height - lp.bottomMargin);
+ break;
+ }
+
+ case Gravity.CENTER_VERTICAL: {
+ final int height = b - t;
+ int childTop = (height - childHeight) / 2;
+
+ // Offset for margins. If things don't fit right because of
+ // bad measurement before, oh well.
+ if (childTop < lp.topMargin) {
+ childTop = lp.topMargin;
+ } else if (childTop + childHeight > height - lp.bottomMargin) {
+ childTop = height - lp.bottomMargin - childHeight;
+ }
+ child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+ break;
+ }
+ }
+
+ if (changeOffset) {
+ setDrawerViewOffset(child, newOffset);
+ }
+
+ final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE;
+ if (child.getVisibility() != newVisibility) {
+ child.setVisibility(newVisibility);
+ }
+ }
+ }
+ mInLayout = false;
+ mFirstLayout = false;
+ }
+
+ @Override
+ public void requestLayout() {
+ if (!mInLayout) {
+ super.requestLayout();
+ }
+ }
+
+ @Override
+ public void computeScroll() {
+ final int childCount = getChildCount();
+ float scrimOpacity = 0;
+ for (int i = 0; i < childCount; i++) {
+ final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen;
+ scrimOpacity = Math.max(scrimOpacity, onscreen);
+ }
+ mScrimOpacity = scrimOpacity;
+
+ // "|" used on purpose; both need to run.
+ if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) {
+ this.postInvalidateOnAnimation();
+ }
+ }
+
+ private static boolean hasOpaqueBackground(View v) {
+ final Drawable bg = v.getBackground();
+ if (bg != null) {
+ return bg.getOpacity() == PixelFormat.OPAQUE;
+ }
+ return false;
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar. Note that this will only be activated if this
+ * DrawerLayout fitsSystemWindows.
+ *
+ * @param bg
+ * Background drawable to draw behind the status bar
+ */
+ public void setStatusBarBackground(Drawable bg) {
+ mStatusBarBackground = bg;
+ invalidate();
+ }
+
+ /**
+ * Gets the drawable used to draw in the insets area for the status bar.
+ *
+ * @return The status bar background drawable, or null if none set
+ */
+ public Drawable getStatusBarBackgroundDrawable() {
+ return mStatusBarBackground;
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar. Note that this will only be activated if this
+ * DrawerLayout fitsSystemWindows.
+ *
+ * @param resId
+ * Resource id of a background drawable to draw behind the status bar
+ */
+ public void setStatusBarBackground(int resId) {
+ mStatusBarBackground = resId != 0 ? getContext().getDrawable(resId) : null;
+ invalidate();
+ }
+
+ /**
+ * Set a drawable to draw in the insets area for the status bar. Note that this will only be activated if this
+ * DrawerLayout fitsSystemWindows.
+ *
+ * @param color
+ * Color to use as a background drawable to draw behind the status bar in 0xAARRGGBB format.
+ */
+ public void setStatusBarBackgroundColor(int color) {
+ mStatusBarBackground = new ColorDrawable(color);
+ invalidate();
+ }
+
+ @Override
+ public void onDraw(Canvas c) {
+ super.onDraw(c);
+ if (mDrawStatusBarBackground && mStatusBarBackground != null) {
+ final int inset = DrawerLayoutCompatApi21.getTopInset(mLastInsets);
+ if (inset > 0) {
+ mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
+ mStatusBarBackground.draw(c);
+ }
+ }
+ }
+
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ final int height = getHeight();
+ final boolean drawingContent = isContentView(child);
+ int clipLeft = 0, clipRight = getWidth();
+
+ final int restoreCount = canvas.save();
+ if (drawingContent) {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View v = getChildAt(i);
+ if (v == child || v.getVisibility() != VISIBLE || !hasOpaqueBackground(v) || !isDrawerView(v) || v.getHeight() < height) {
+ continue;
+ }
+
+ if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) {
+ final int vright = v.getRight();
+ if (vright > clipLeft) clipLeft = vright;
+ } else {
+ final int vleft = v.getLeft();
+ if (vleft < clipRight) clipRight = vleft;
+ }
+ }
+ canvas.clipRect(clipLeft, 0, clipRight, getHeight());
+ }
+ final boolean result = super.drawChild(canvas, child, drawingTime);
+ canvas.restoreToCount(restoreCount);
+
+ if (mScrimOpacity > 0 && drawingContent) {
+ final int baseAlpha = (mScrimColor & 0xff000000) >>> 24;
+ final int imag = (int) (baseAlpha * mScrimOpacity);
+ final int color = imag << 24 | (mScrimColor & 0xffffff);
+ mScrimPaint.setColor(color);
+
+ canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint);
+ } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
+ final int shadowWidth = mShadowLeft.getIntrinsicWidth();
+ final int childRight = child.getRight();
+ final int drawerPeekDistance = mLeftDragger.getEdgeSize();
+ final float alpha = Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f));
+ mShadowLeft.setBounds(childRight, child.getTop(), childRight + shadowWidth, child.getBottom());
+ mShadowLeft.setAlpha((int) (0xff * alpha));
+ mShadowLeft.draw(canvas);
+ } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) {
+ final int shadowWidth = mShadowRight.getIntrinsicWidth();
+ final int childLeft = child.getLeft();
+ final int showing = getWidth() - childLeft;
+ final int drawerPeekDistance = mRightDragger.getEdgeSize();
+ final float alpha = Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f));
+ mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), childLeft, child.getBottom());
+ mShadowRight.setAlpha((int) (0xff * alpha));
+ mShadowRight.draw(canvas);
+ }
+ return result;
+ }
+
+ boolean isContentView(View child) {
+ return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY;
+ }
+
+ boolean isDrawerView(View child) {
+ final int gravity = ((LayoutParams) child.getLayoutParams()).gravity;
+ final int absGravity = Gravity.getAbsoluteGravity(gravity, child.getLayoutDirection());
+ return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+
+ // "|" used deliberately here; both methods should be invoked.
+ final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | mRightDragger.shouldInterceptTouchEvent(ev);
+
+ boolean interceptForTap = false;
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ if (mScrimOpacity > 0) {
+ final View child = mLeftDragger.findTopChildUnder((int) x, (int) y);
+ if (child != null && isContentView(child)) {
+ interceptForTap = true;
+ }
+ }
+ mChildrenCanceledTouch = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ // If we cross the touch slop, don't perform the delayed peek for an edge touch.
+ if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) {
+ mLeftCallback.removeCallbacks();
+ mRightCallback.removeCallbacks();
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP: {
+ closeDrawers(true);
+ mChildrenCanceledTouch = false;
+ }
+ }
+
+ return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch;
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ mLeftDragger.processTouchEvent(ev);
+ mRightDragger.processTouchEvent(ev);
+
+ final int action = ev.getAction();
+ boolean wantTouchEvents = true;
+
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ mInitialMotionX = x;
+ mInitialMotionY = y;
+ mChildrenCanceledTouch = false;
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ boolean peekingOnly = true;
+ final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y);
+ if (touchedView != null && isContentView(touchedView)) {
+ final float dx = x - mInitialMotionX;
+ final float dy = y - mInitialMotionY;
+ final int slop = mLeftDragger.getTouchSlop();
+ if (dx * dx + dy * dy < slop * slop) {
+ // Taps close a dimmed open drawer but only if it isn't locked open.
+ final View openDrawer = findOpenDrawer();
+ if (openDrawer != null) {
+ peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN;
+ }
+ }
+ }
+ closeDrawers(peekingOnly);
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ closeDrawers(true);
+ mChildrenCanceledTouch = false;
+ break;
+ }
+ }
+
+ return wantTouchEvents;
+ }
+
+ @Override
+ public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+ if (CHILDREN_DISALLOW_INTERCEPT/*
+ * || (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) &&
+ * !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))
+ */) {
+ // If we have an edge touch we want to skip this and track it for later instead.
+ super.requestDisallowInterceptTouchEvent(disallowIntercept);
+ }
+ if (disallowIntercept) {
+ closeDrawers(true);
+ }
+ }
+
+ /**
+ * Close all currently open drawer views by animating them out of view.
+ */
+ public void closeDrawers() {
+ closeDrawers(false);
+ }
+
+ void closeDrawers(boolean peekingOnly) {
+ boolean needsInvalidate = false;
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+ if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) {
+ continue;
+ }
+
+ final int childWidth = child.getWidth();
+
+ if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
+ needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, -childWidth, child.getTop());
+ } else {
+ needsInvalidate |= mRightDragger.smoothSlideViewTo(child, getWidth(), child.getTop());
+ }
+
+ lp.isPeeking = false;
+ }
+
+ mLeftCallback.removeCallbacks();
+ mRightCallback.removeCallbacks();
+
+ if (needsInvalidate) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Open the specified drawer view by animating it into view.
+ *
+ * @param drawerView
+ * Drawer view to open
+ */
+ public void openDrawer(View drawerView) {
+ if (!isDrawerView(drawerView)) {
+ throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
+ }
+
+ if (mFirstLayout) {
+ final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
+ lp.onScreen = 1.f;
+ lp.knownOpen = true;
+
+ updateChildrenImportantForAccessibility(drawerView, true);
+ } else {
+ if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
+ mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop());
+ } else {
+ mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), drawerView.getTop());
+ }
+ }
+ invalidate();
+ }
+
+ /**
+ * Open the specified drawer by animating it out of view.
+ *
+ * @param gravity
+ * Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. Gravity.START or Gravity.END may
+ * also be used.
+ */
+ public void openDrawer(int gravity) {
+ final View drawerView = findDrawerWithGravity(gravity);
+ if (drawerView == null) {
+ throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity));
+ }
+ openDrawer(drawerView);
+ }
+
+ /**
+ * Close the specified drawer view by animating it into view.
+ *
+ * @param drawerView
+ * Drawer view to close
+ */
+ public void closeDrawer(View drawerView) {
+ if (!isDrawerView(drawerView)) {
+ throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer");
+ }
+
+ if (mFirstLayout) {
+ final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams();
+ lp.onScreen = 0.f;
+ lp.knownOpen = false;
+ } else {
+ if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) {
+ mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), drawerView.getTop());
+ } else {
+ mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop());
+ }
+ }
+ invalidate();
+ }
+
+ /**
+ * Close the specified drawer by animating it out of view.
+ *
+ * @param gravity
+ * Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. Gravity.START or Gravity.END may
+ * also be used.
+ */
+ public void closeDrawer(int gravity) {
+ final View drawerView = findDrawerWithGravity(gravity);
+ if (drawerView == null) {
+ throw new IllegalArgumentException("No drawer view found with gravity " + gravityToString(gravity));
+ }
+ closeDrawer(drawerView);
+ }
+
+ /**
+ * Check if the given drawer view is currently in an open state. To be considered "open" the drawer must have
+ * settled into its fully visible state. To check for partial visibility use
+ * {@link #isDrawerVisible(android.view.View)}.
+ *
+ * @param drawer
+ * Drawer view to check
+ * @return true if the given drawer view is in an open state
+ * @see #isDrawerVisible(android.view.View)
+ */
+ public boolean isDrawerOpen(View drawer) {
+ if (!isDrawerView(drawer)) {
+ throw new IllegalArgumentException("View " + drawer + " is not a drawer");
+ }
+ return ((LayoutParams) drawer.getLayoutParams()).knownOpen;
+ }
+
+ /**
+ * Check if the given drawer view is currently in an open state. To be considered "open" the drawer must have
+ * settled into its fully visible state. If there is no drawer with the given gravity this method will return false.
+ *
+ * @param drawerGravity
+ * Gravity of the drawer to check
+ * @return true if the given drawer view is in an open state
+ */
+ public boolean isDrawerOpen(int drawerGravity) {
+ final View drawerView = findDrawerWithGravity(drawerGravity);
+ if (drawerView != null) {
+ return isDrawerOpen(drawerView);
+ }
+ return false;
+ }
+
+ /**
+ * Check if a given drawer view is currently visible on-screen. The drawer may be only peeking onto the screen,
+ * fully extended, or anywhere inbetween.
+ *
+ * @param drawer
+ * Drawer view to check
+ * @return true if the given drawer is visible on-screen
+ * @see #isDrawerOpen(android.view.View)
+ */
+ public boolean isDrawerVisible(View drawer) {
+ if (!isDrawerView(drawer)) {
+ throw new IllegalArgumentException("View " + drawer + " is not a drawer");
+ }
+ return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0;
+ }
+
+ /**
+ * Check if a given drawer view is currently visible on-screen. The drawer may be only peeking onto the screen,
+ * fully extended, or anywhere in between. If there is no drawer with the given gravity this method will return
+ * false.
+ *
+ * @param drawerGravity
+ * Gravity of the drawer to check
+ * @return true if the given drawer is visible on-screen
+ */
+ public boolean isDrawerVisible(int drawerGravity) {
+ final View drawerView = findDrawerWithGravity(drawerGravity);
+ if (drawerView != null) {
+ return isDrawerVisible(drawerView);
+ }
+ return false;
+ }
+
+ private boolean hasPeekingDrawer() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+ if (lp.isPeeking) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+ return new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.MATCH_PARENT);
+ }
+
+ @Override
+ protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams ? new LayoutParams((LayoutParams) p) : p instanceof ViewGroup.MarginLayoutParams ? new LayoutParams(
+ (MarginLayoutParams) p) : new LayoutParams(p);
+ }
+
+ @Override
+ protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+ return p instanceof LayoutParams && super.checkLayoutParams(p);
+ }
+
+ @Override
+ public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+ return new LayoutParams(getContext(), attrs);
+ }
+
+ private boolean hasVisibleDrawer() {
+ return findVisibleDrawer() != null;
+ }
+
+ View findVisibleDrawer() {
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ if (isDrawerView(child) && isDrawerVisible(child)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ void cancelChildViewTouch() {
+ // Cancel child touches
+ if (!mChildrenCanceledTouch) {
+ final long now = SystemClock.uptimeMillis();
+ final MotionEvent cancelEvent = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ getChildAt(i).dispatchTouchEvent(cancelEvent);
+ }
+ cancelEvent.recycle();
+ mChildrenCanceledTouch = true;
+ }
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) {
+ event.startTracking();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ final View visibleDrawer = findVisibleDrawer();
+ if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) {
+ closeDrawers();
+ }
+ return visibleDrawer != null;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Parcelable state) {
+ final SavedState ss = (SavedState) state;
+ super.onRestoreInstanceState(ss.getSuperState());
+
+ if (ss.openDrawerGravity != Gravity.NO_GRAVITY) {
+ final View toOpen = findDrawerWithGravity(ss.openDrawerGravity);
+ if (toOpen != null) {
+ openDrawer(toOpen);
+ }
+ }
+
+ setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT);
+ setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT);
+ }
+
+ @Override
+ protected Parcelable onSaveInstanceState() {
+ final Parcelable superState = super.onSaveInstanceState();
+ final SavedState ss = new SavedState(superState);
+
+ final View openDrawer = findOpenDrawer();
+ if (openDrawer != null) {
+ ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity;
+ }
+
+ ss.lockModeLeft = mLockModeLeft;
+ ss.lockModeRight = mLockModeRight;
+
+ return ss;
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+
+ final View openDrawer = findOpenDrawer();
+ if (openDrawer != null || isDrawerView(child)) {
+ // A drawer is already open or the new view is a drawer, so the
+ // new view should start out hidden.
+ child.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ } else {
+ // Otherwise this is a content view and no drawer is open, so the
+ // new view should start out visible.
+ child.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+ }
+
+ // We only need a delegate here if the framework doesn't understand
+ // NO_HIDE_DESCENDANTS importance.
+ if (!CAN_HIDE_DESCENDANTS) {
+ child.setAccessibilityDelegate(mChildAccessibilityDelegate);
+ }
+ }
+
+ static boolean includeChildForAccessibility(View child) {
+ // If the child is not important for accessibility we make
+ // sure this hides the entire subtree rooted at it as the
+ // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not
+ // supported on older platforms but we want to hide the entire
+ // content and not opened drawers if a drawer is opened.
+ return child.getImportantForAccessibility() != View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ && child.getImportantForAccessibility() != View.IMPORTANT_FOR_ACCESSIBILITY_NO;
+ }
+
+ /**
+ * State persisted across instances
+ */
+ protected static class SavedState extends BaseSavedState {
+ int openDrawerGravity = Gravity.NO_GRAVITY;
+ int lockModeLeft = LOCK_MODE_UNLOCKED;
+ int lockModeRight = LOCK_MODE_UNLOCKED;
+
+ public SavedState(Parcel in) {
+ super(in);
+ openDrawerGravity = in.readInt();
+ }
+
+ public SavedState(Parcelable superState) {
+ super(superState);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
+ dest.writeInt(openDrawerGravity);
+ }
+ }
+
+ private class ViewDragCallback extends ViewDragHelper.Callback {
+ private final int mAbsGravity;
+ private ViewDragHelper mDragger;
+
+ private final Runnable mPeekRunnable = new Runnable() {
+ @Override
+ public void run() {
+ peekDrawer();
+ }
+ };
+
+ public ViewDragCallback(int gravity) {
+ mAbsGravity = gravity;
+ }
+
+ public void setDragger(ViewDragHelper dragger) {
+ mDragger = dragger;
+ }
+
+ public void removeCallbacks() {
+ DrawerLayout.this.removeCallbacks(mPeekRunnable);
+ }
+
+ @Override
+ public boolean tryCaptureView(View child, int pointerId) {
+ // Only capture views where the gravity matches what we're looking for.
+ // This lets us use two ViewDragHelpers, one for each side drawer.
+ return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED;
+ }
+
+ @Override
+ public void onViewDragStateChanged(int state) {
+ updateDrawerState(state, mDragger.getCapturedView());
+ }
+
+ @Override
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ float offset;
+ final int childWidth = changedView.getWidth();
+
+ // This reverses the positioning shown in onLayout.
+ if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) {
+ offset = (float) (childWidth + left) / childWidth;
+ } else {
+ final int width = getWidth();
+ offset = (float) (width - left) / childWidth;
+ }
+ setDrawerViewOffset(changedView, offset);
+ changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE);
+ invalidate();
+ }
+
+ @Override
+ public void onViewCaptured(View capturedChild, int activePointerId) {
+ final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams();
+ lp.isPeeking = false;
+
+ closeOtherDrawer();
+ }
+
+ private void closeOtherDrawer() {
+ final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT;
+ final View toClose = findDrawerWithGravity(otherGrav);
+ if (toClose != null) {
+ closeDrawer(toClose);
+ }
+ }
+
+ @Override
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ // Offset is how open the drawer is, therefore left/right values
+ // are reversed from one another.
+ final float offset = getDrawerViewOffset(releasedChild);
+ final int childWidth = releasedChild.getWidth();
+
+ int left;
+ if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) {
+ left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth;
+ } else {
+ final int width = getWidth();
+ left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width;
+ }
+
+ mDragger.settleCapturedViewAt(left, releasedChild.getTop());
+ invalidate();
+ }
+
+ @Override
+ public void onEdgeTouched(int edgeFlags, int pointerId) {
+ postDelayed(mPeekRunnable, PEEK_DELAY);
+ }
+
+ void peekDrawer() {
+ final View toCapture;
+ final int childLeft;
+ final int peekDistance = mDragger.getEdgeSize();
+ final boolean leftEdge = mAbsGravity == Gravity.LEFT;
+ if (leftEdge) {
+ toCapture = findDrawerWithGravity(Gravity.LEFT);
+ childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance;
+ } else {
+ toCapture = findDrawerWithGravity(Gravity.RIGHT);
+ childLeft = getWidth() - peekDistance;
+ }
+ // Only peek if it would mean making the drawer more visible and the drawer isn't locked
+ if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || (!leftEdge && toCapture.getLeft() > childLeft))
+ && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) {
+ final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams();
+ mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop());
+ lp.isPeeking = true;
+ invalidate();
+
+ closeOtherDrawer();
+
+ cancelChildViewTouch();
+ }
+ }
+
+ @Override
+ public boolean onEdgeLock(int edgeFlags) {
+ if (ALLOW_EDGE_LOCK) {
+ final View drawer = findDrawerWithGravity(mAbsGravity);
+ if (drawer != null && !isDrawerOpen(drawer)) {
+ closeDrawer(drawer);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+ final View toCapture;
+ if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) {
+ toCapture = findDrawerWithGravity(Gravity.LEFT);
+ } else {
+ toCapture = findDrawerWithGravity(Gravity.RIGHT);
+ }
+
+ if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) {
+ mDragger.captureChildView(toCapture, pointerId);
+ }
+ }
+
+ @Override
+ public int getViewHorizontalDragRange(View child) {
+ return isDrawerView(child) ? child.getWidth() : 0;
+ }
+
+ @Override
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) {
+ return Math.max(-child.getWidth(), Math.min(left, 0));
+ } else {
+ final int width = getWidth();
+ return Math.max(width - child.getWidth(), Math.min(left, width));
+ }
+ }
+
+ @Override
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ return child.getTop();
+ }
+ }
+
+ public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+
+ public int gravity = Gravity.NO_GRAVITY;
+ float onScreen;
+ boolean isPeeking;
+ boolean knownOpen;
+
+ public LayoutParams(Context c, AttributeSet attrs) {
+ super(c, attrs);
+
+ final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS);
+ this.gravity = a.getInt(0, Gravity.NO_GRAVITY);
+ a.recycle();
+ }
+
+ public LayoutParams(int width, int height) {
+ super(width, height);
+ }
+
+ public LayoutParams(int width, int height, int gravity) {
+ this(width, height);
+ this.gravity = gravity;
+ }
+
+ public LayoutParams(LayoutParams source) {
+ super(source);
+ this.gravity = source.gravity;
+ }
+
+ public LayoutParams(ViewGroup.LayoutParams source) {
+ super(source);
+ }
+
+ public LayoutParams(ViewGroup.MarginLayoutParams source) {
+ super(source);
+ }
+ }
+
+ class AccessibilityDelegate extends android.view.View.AccessibilityDelegate {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+
+ info.setClassName(DrawerLayout.class.getName());
+
+ // This view reports itself as focusable so that it can intercept
+ // the back button, but we should prevent this view from reporting
+ // itself as focusable to accessibility services.
+ info.setFocusable(false);
+ info.setFocused(false);
+ }
+
+ @Override
+ public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
+ super.onInitializeAccessibilityEvent(host, event);
+
+ event.setClassName(DrawerLayout.class.getName());
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
+ // Special case to handle window state change events. As far as
+ // accessibility services are concerned, state changes from
+ // DrawerLayout invalidate the entire contents of the screen (like
+ // an Activity or Dialog) and they should announce the title of the
+ // new content.
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ final List eventText = event.getText();
+ final View visibleDrawer = findVisibleDrawer();
+ if (visibleDrawer != null) {
+ final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer);
+ final CharSequence title = getDrawerTitle(edgeGravity);
+ if (title != null) {
+ eventText.add(title);
+ }
+ }
+
+ return true;
+ }
+
+ return super.dispatchPopulateAccessibilityEvent(host, event);
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, AccessibilityEvent event) {
+ if (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) {
+ return super.onRequestSendAccessibilityEvent(host, child, event);
+ }
+ return false;
+ }
+ }
+
+ final class ChildAccessibilityDelegate extends AccessibilityDelegate {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View child, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(child, info);
+
+ if (!includeChildForAccessibility(child)) {
+ // If we are ignoring the sub-tree rooted at the child,
+ // break the connection to the rest of the node tree.
+ // For details refer to includeChildForAccessibility.
+ info.setParent(null);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/termux/drawer/DrawerLayoutCompatApi21.java b/app/src/main/java/com/termux/drawer/DrawerLayoutCompatApi21.java
new file mode 100644
index 0000000000..f3fe02774b
--- /dev/null
+++ b/app/src/main/java/com/termux/drawer/DrawerLayoutCompatApi21.java
@@ -0,0 +1,88 @@
+package com.termux.drawer;
+
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowInsets;
+
+/**
+ * Provides functionality for DrawerLayout unique to API 21
+ */
+@SuppressLint("RtlHardcoded")
+@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+class DrawerLayoutCompatApi21 {
+
+ private static final int[] THEME_ATTRS = { android.R.attr.colorPrimaryDark };
+
+ public static void configureApplyInsets(DrawerLayout drawerLayout) {
+ drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
+ drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
+ }
+
+ public static void dispatchChildInsets(View child, Object insets, int gravity) {
+ WindowInsets wi = (WindowInsets) insets;
+ if (gravity == Gravity.LEFT) {
+ wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
+ } else if (gravity == Gravity.RIGHT) {
+ wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
+ }
+ child.dispatchApplyWindowInsets(wi);
+ }
+
+ public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, int gravity) {
+ WindowInsets wi = (WindowInsets) insets;
+ if (gravity == Gravity.LEFT) {
+ wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom());
+ } else if (gravity == Gravity.RIGHT) {
+ wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom());
+ }
+ lp.leftMargin = wi.getSystemWindowInsetLeft();
+ lp.topMargin = wi.getSystemWindowInsetTop();
+ lp.rightMargin = wi.getSystemWindowInsetRight();
+ lp.bottomMargin = wi.getSystemWindowInsetBottom();
+ }
+
+ public static int getTopInset(Object insets) {
+ return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0;
+ }
+
+ public static Drawable getDefaultStatusBarBackground(Context context) {
+ final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
+ try {
+ return a.getDrawable(0);
+ } finally {
+ a.recycle();
+ }
+ }
+
+ static class InsetsListener implements View.OnApplyWindowInsetsListener {
+ @Override
+ public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ final DrawerLayout drawerLayout = (DrawerLayout) v;
+ drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
+ return insets.consumeSystemWindowInsets();
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/termux/drawer/ViewDragHelper.java b/app/src/main/java/com/termux/drawer/ViewDragHelper.java
new file mode 100644
index 0000000000..4b519fdad4
--- /dev/null
+++ b/app/src/main/java/com/termux/drawer/ViewDragHelper.java
@@ -0,0 +1,1525 @@
+package com.termux.drawer;
+
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.animation.Interpolator;
+import android.widget.Scroller;
+
+import java.util.Arrays;
+
+/**
+ * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number of useful operations and state
+ * tracking for allowing a user to drag and reposition views within their parent ViewGroup.
+ */
+public class ViewDragHelper {
+
+ /**
+ * A null/invalid pointer ID.
+ */
+ public static final int INVALID_POINTER = -1;
+
+ /**
+ * A view is not currently being dragged or animating as a result of a fling/snap.
+ */
+ public static final int STATE_IDLE = 0;
+
+ /**
+ * A view is currently being dragged. The position is currently changing as a result of user input or simulated user
+ * input.
+ */
+ public static final int STATE_DRAGGING = 1;
+
+ /**
+ * A view is currently settling into place as a result of a fling or predefined non-interactive motion.
+ */
+ public static final int STATE_SETTLING = 2;
+
+ /**
+ * Edge flag indicating that the left edge should be affected.
+ */
+ public static final int EDGE_LEFT = 1 << 0;
+
+ /**
+ * Edge flag indicating that the right edge should be affected.
+ */
+ public static final int EDGE_RIGHT = 1 << 1;
+
+ /**
+ * Edge flag indicating that the top edge should be affected.
+ */
+ public static final int EDGE_TOP = 1 << 2;
+
+ /**
+ * Edge flag indicating that the bottom edge should be affected.
+ */
+ public static final int EDGE_BOTTOM = 1 << 3;
+
+ /**
+ * Edge flag set indicating all edges should be affected.
+ */
+ public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM;
+
+ /**
+ * Indicates that a check should occur along the horizontal axis
+ */
+ public static final int DIRECTION_HORIZONTAL = 1 << 0;
+
+ /**
+ * Indicates that a check should occur along the vertical axis
+ */
+ public static final int DIRECTION_VERTICAL = 1 << 1;
+
+ /**
+ * Indicates that a check should occur along all axes
+ */
+ public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL;
+
+ private static final int EDGE_SIZE = 20; // dp
+
+ private static final int BASE_SETTLE_DURATION = 256; // ms
+ private static final int MAX_SETTLE_DURATION = 600; // ms
+
+ // Current drag state; idle, dragging or settling
+ private int mDragState;
+
+ // Distance to travel before a drag may begin
+ private int mTouchSlop;
+
+ // Last known position/pointer tracking
+ private int mActivePointerId = INVALID_POINTER;
+ private float[] mInitialMotionX;
+ private float[] mInitialMotionY;
+ private float[] mLastMotionX;
+ private float[] mLastMotionY;
+ private int[] mInitialEdgesTouched;
+ private int[] mEdgeDragsInProgress;
+ private int[] mEdgeDragsLocked;
+ private int mPointersDown;
+
+ private VelocityTracker mVelocityTracker;
+ private float mMaxVelocity;
+ private float mMinVelocity;
+
+ private int mEdgeSize;
+ private int mTrackingEdges;
+
+ private Scroller mScroller;
+
+ private final Callback mCallback;
+
+ private View mCapturedView;
+ private boolean mReleaseInProgress;
+
+ private final ViewGroup mParentView;
+
+ /**
+ * A Callback is used as a communication channel with the ViewDragHelper back to the parent view using it.
+ * on*methods are invoked on siginficant events and several accessor methods are expected to provide
+ * the ViewDragHelper with more information about the state of the parent view upon request. The callback also makes
+ * decisions governing the range and draggability of child views.
+ */
+ public static abstract class Callback {
+ /**
+ * Called when the drag state changes. See the STATE_* constants for more information.
+ *
+ * @param state
+ * The new drag state
+ *
+ * @see #STATE_IDLE
+ * @see #STATE_DRAGGING
+ * @see #STATE_SETTLING
+ */
+ public void onViewDragStateChanged(int state) {
+ // Abstract.
+ }
+
+ /**
+ * Called when the captured view's position changes as the result of a drag or settle.
+ *
+ * @param changedView
+ * View whose position changed
+ * @param left
+ * New X coordinate of the left edge of the view
+ * @param top
+ * New Y coordinate of the top edge of the view
+ * @param dx
+ * Change in X position from the last call
+ * @param dy
+ * Change in Y position from the last call
+ */
+ public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
+ // Abstract.
+ }
+
+ /**
+ * Called when a child view is captured for dragging or settling. The ID of the pointer currently dragging the
+ * captured view is supplied. If activePointerId is identified as {@link #INVALID_POINTER} the capture is
+ * programmatic instead of pointer-initiated.
+ *
+ * @param capturedChild
+ * Child view that was captured
+ * @param activePointerId
+ * Pointer id tracking the child capture
+ */
+ public void onViewCaptured(View capturedChild, int activePointerId) {
+ // Abstract.
+ }
+
+ /**
+ * Called when the child view is no longer being actively dragged. The fling velocity is also supplied, if
+ * relevant. The velocity values may be clamped to system minimums or maximums.
+ *
+ *
+ * Calling code may decide to fling or otherwise release the view to let it settle into place. It should do so
+ * using {@link #settleCapturedViewAt(int, int)} or {@link #flingCapturedView(int, int, int, int)}. If the
+ * Callback invokes one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} and the view
+ * capture will not fully end until it comes to a complete stop. If neither of these methods is invoked before
+ * onViewReleased returns, the view will stop in place and the ViewDragHelper will return to
+ * {@link #STATE_IDLE}.
+ *
+ *
+ * @param releasedChild
+ * The captured child view now being released
+ * @param xvel
+ * X velocity of the pointer as it left the screen in pixels per second.
+ * @param yvel
+ * Y velocity of the pointer as it left the screen in pixels per second.
+ */
+ public void onViewReleased(View releasedChild, float xvel, float yvel) {
+ // Abstract.
+ }
+
+ /**
+ * Called when one of the subscribed edges in the parent view has been touched by the user while no child view
+ * is currently captured.
+ *
+ * @param edgeFlags
+ * A combination of edge flags describing the edge(s) currently touched
+ * @param pointerId
+ * ID of the pointer touching the described edge(s)
+ * @see #EDGE_LEFT
+ * @see #EDGE_TOP
+ * @see #EDGE_RIGHT
+ * @see #EDGE_BOTTOM
+ */
+ public void onEdgeTouched(int edgeFlags, int pointerId) {
+ // Abstract.
+ }
+
+ /**
+ * Called when the given edge may become locked. This can happen if an edge drag was preliminarily rejected
+ * before beginning, but after {@link #onEdgeTouched(int, int)} was called. This method should return true to
+ * lock this edge or false to leave it unlocked. The default behavior is to leave edges unlocked.
+ *
+ * @param edgeFlags
+ * A combination of edge flags describing the edge(s) locked
+ * @return true to lock the edge, false to leave it unlocked
+ */
+ public boolean onEdgeLock(int edgeFlags) {
+ return false;
+ }
+
+ /**
+ * Called when the user has started a deliberate drag away from one of the subscribed edges in the parent view
+ * while no child view is currently captured.
+ *
+ * @param edgeFlags
+ * A combination of edge flags describing the edge(s) dragged
+ * @param pointerId
+ * ID of the pointer touching the described edge(s)
+ * @see #EDGE_LEFT
+ * @see #EDGE_TOP
+ * @see #EDGE_RIGHT
+ * @see #EDGE_BOTTOM
+ */
+ public void onEdgeDragStarted(int edgeFlags, int pointerId) {
+ // Abstract.
+ }
+
+ /**
+ * Called to determine the Z-order of child views.
+ *
+ * @param index
+ * the ordered position to query for
+ * @return index of the view that should be ordered at position index
+ */
+ public int getOrderedChildIndex(int index) {
+ return index;
+ }
+
+ /**
+ * Return the magnitude of a draggable child view's horizontal range of motion in pixels. This method should
+ * return 0 for views that cannot move horizontally.
+ *
+ * @param child
+ * Child view to check
+ * @return range of horizontal motion in pixels
+ */
+ public int getViewHorizontalDragRange(View child) {
+ return 0;
+ }
+
+ /**
+ * Return the magnitude of a draggable child view's vertical range of motion in pixels. This method should
+ * return 0 for views that cannot move vertically.
+ *
+ * @param child
+ * Child view to check
+ * @return range of vertical motion in pixels
+ */
+ public int getViewVerticalDragRange(View child) {
+ return 0;
+ }
+
+ /**
+ * Called when the user's input indicates that they want to capture the given child view with the pointer
+ * indicated by pointerId. The callback should return true if the user is permitted to drag the given view with
+ * the indicated pointer.
+ *
+ *
+ * ViewDragHelper may call this method multiple times for the same view even if the view is already captured;
+ * this indicates that a new pointer is trying to take control of the view.
+ *
+ *
+ *
+ * If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} will follow if the
+ * capture is successful.
+ *
+ *
+ * @param child
+ * Child the user is attempting to capture
+ * @param pointerId
+ * ID of the pointer attempting the capture
+ * @return true if capture should be allowed, false otherwise
+ */
+ public abstract boolean tryCaptureView(View child, int pointerId);
+
+ /**
+ * Restrict the motion of the dragged child view along the horizontal axis. The default implementation does not
+ * allow horizontal motion; the extending class must override this method and provide the desired clamping.
+ *
+ *
+ * @param child
+ * Child view being dragged
+ * @param left
+ * Attempted motion along the X axis
+ * @param dx
+ * Proposed change in position for left
+ * @return The new clamped position for left
+ */
+ public int clampViewPositionHorizontal(View child, int left, int dx) {
+ return 0;
+ }
+
+ /**
+ * Restrict the motion of the dragged child view along the vertical axis. The default implementation does not
+ * allow vertical motion; the extending class must override this method and provide the desired clamping.
+ *
+ *
+ * @param child
+ * Child view being dragged
+ * @param top
+ * Attempted motion along the Y axis
+ * @param dy
+ * Proposed change in position for top
+ * @return The new clamped position for top
+ */
+ public int clampViewPositionVertical(View child, int top, int dy) {
+ return 0;
+ }
+ }
+
+ /**
+ * Interpolator defining the animation curve for mScroller
+ */
+ private static final Interpolator sInterpolator = new Interpolator() {
+ @Override
+ public float getInterpolation(float t) {
+ t -= 1.0f;
+ return t * t * t * t * t + 1.0f;
+ }
+ };
+
+ private final Runnable mSetIdleRunnable = new Runnable() {
+ @Override
+ public void run() {
+ setDragState(STATE_IDLE);
+ }
+ };
+
+ /**
+ * Factory method to create a new ViewDragHelper.
+ *
+ * @param forParent
+ * Parent view to monitor
+ * @param cb
+ * Callback to provide information and receive events
+ * @return a new ViewDragHelper instance
+ */
+ public static ViewDragHelper create(ViewGroup forParent, Callback cb) {
+ return new ViewDragHelper(forParent.getContext(), forParent, cb);
+ }
+
+ /**
+ * Factory method to create a new ViewDragHelper.
+ *
+ * @param forParent
+ * Parent view to monitor
+ * @param sensitivity
+ * Multiplier for how sensitive the helper should be about detecting the start of a drag. Larger values
+ * are more sensitive. 1.0f is normal.
+ * @param cb
+ * Callback to provide information and receive events
+ * @return a new ViewDragHelper instance
+ */
+ public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) {
+ final ViewDragHelper helper = create(forParent, cb);
+ helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity));
+ return helper;
+ }
+
+ /**
+ * Apps should use ViewDragHelper.create() to get a new instance. This will allow VDH to use internal compatibility
+ * implementations for different platform versions.
+ *
+ * @param context
+ * Context to initialize config-dependent params from
+ * @param forParent
+ * Parent view to monitor
+ */
+ private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) {
+ if (forParent == null) {
+ throw new IllegalArgumentException("Parent view may not be null");
+ }
+ if (cb == null) {
+ throw new IllegalArgumentException("Callback may not be null");
+ }
+
+ mParentView = forParent;
+ mCallback = cb;
+
+ final ViewConfiguration vc = ViewConfiguration.get(context);
+ final float density = context.getResources().getDisplayMetrics().density;
+ mEdgeSize = (int) (EDGE_SIZE * density + 0.5f);
+
+ mTouchSlop = vc.getScaledTouchSlop();
+ mMaxVelocity = vc.getScaledMaximumFlingVelocity();
+ mMinVelocity = vc.getScaledMinimumFlingVelocity();
+ mScroller = new Scroller(context, sInterpolator);
+ }
+
+ /**
+ * Set the minimum velocity that will be detected as having a magnitude greater than zero in pixels per second.
+ * Callback methods accepting a velocity will be clamped appropriately.
+ *
+ * @param minVel
+ * Minimum velocity to detect
+ */
+ public void setMinVelocity(float minVel) {
+ mMinVelocity = minVel;
+ }
+
+ /**
+ * Return the currently configured minimum velocity. Any flings with a magnitude less than this value in pixels per
+ * second. Callback methods accepting a velocity will receive zero as a velocity value if the real detected velocity
+ * was below this threshold.
+ *
+ * @return the minimum velocity that will be detected
+ */
+ public float getMinVelocity() {
+ return mMinVelocity;
+ }
+
+ /**
+ * Retrieve the current drag state of this helper. This will return one of {@link #STATE_IDLE},
+ * {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
+ *
+ * @return The current drag state
+ */
+ public int getViewDragState() {
+ return mDragState;
+ }
+
+ /**
+ * Enable edge tracking for the selected edges of the parent view. The callback's
+ * {@link Callback#onEdgeTouched(int, int)} and {@link Callback#onEdgeDragStarted(int, int)} methods will only be
+ * invoked for edges for which edge tracking has been enabled.
+ *
+ * @param edgeFlags
+ * Combination of edge flags describing the edges to watch
+ * @see #EDGE_LEFT
+ * @see #EDGE_TOP
+ * @see #EDGE_RIGHT
+ * @see #EDGE_BOTTOM
+ */
+ public void setEdgeTrackingEnabled(int edgeFlags) {
+ mTrackingEdges = edgeFlags;
+ }
+
+ /**
+ * Return the size of an edge. This is the range in pixels along the edges of this view that will actively detect
+ * edge touches or drags if edge tracking is enabled.
+ *
+ * @return The size of an edge in pixels
+ * @see #setEdgeTrackingEnabled(int)
+ */
+ public int getEdgeSize() {
+ return mEdgeSize;
+ }
+
+ /**
+ * Capture a specific child view for dragging within the parent. The callback will be notified but
+ * {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to capture this view.
+ *
+ * @param childView
+ * Child view to capture
+ * @param activePointerId
+ * ID of the pointer that is dragging the captured child view
+ */
+ public void captureChildView(View childView, int activePointerId) {
+ if (childView.getParent() != mParentView) {
+ throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + "of the ViewDragHelper's tracked parent view ("
+ + mParentView + ")");
+ }
+
+ mCapturedView = childView;
+ mActivePointerId = activePointerId;
+ mCallback.onViewCaptured(childView, activePointerId);
+ setDragState(STATE_DRAGGING);
+ }
+
+ /**
+ * @return The currently captured view, or null if no view has been captured.
+ */
+ public View getCapturedView() {
+ return mCapturedView;
+ }
+
+ /**
+ * @return The ID of the pointer currently dragging the captured view, or {@link #INVALID_POINTER}.
+ */
+ public int getActivePointerId() {
+ return mActivePointerId;
+ }
+
+ /**
+ * @return The minimum distance in pixels that the user must travel to initiate a drag
+ */
+ public int getTouchSlop() {
+ return mTouchSlop;
+ }
+
+ /**
+ * The result of a call to this method is equivalent to {@link #processTouchEvent(android.view.MotionEvent)}
+ * receiving an ACTION_CANCEL event.
+ */
+ public void cancel() {
+ mActivePointerId = INVALID_POINTER;
+ clearMotionHistory();
+
+ if (mVelocityTracker != null) {
+ mVelocityTracker.recycle();
+ mVelocityTracker = null;
+ }
+ }
+
+ /**
+ * {@link #cancel()}, but also abort all motion in progress and snap to the end of any animation.
+ */
+ public void abort() {
+ cancel();
+ if (mDragState == STATE_SETTLING) {
+ final int oldX = mScroller.getCurrX();
+ final int oldY = mScroller.getCurrY();
+ mScroller.abortAnimation();
+ final int newX = mScroller.getCurrX();
+ final int newY = mScroller.getCurrY();
+ mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY);
+ }
+ setDragState(STATE_IDLE);
+ }
+
+ /**
+ * Animate the view child to the given (left, top) position. If this method returns true, the caller
+ * should invoke {@link #continueSettling(boolean)} on each subsequent frame to continue the motion until it returns
+ * false. If this method returns false there is no further work to do to complete the movement.
+ *
+ *
+ * This operation does not count as a capture event, though {@link #getCapturedView()} will still report the sliding
+ * view while the slide is in progress.
+ *
+ *
+ * @param child
+ * Child view to capture and animate
+ * @param finalLeft
+ * Final left position of child
+ * @param finalTop
+ * Final top position of child
+ * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+ */
+ public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) {
+ mCapturedView = child;
+ mActivePointerId = INVALID_POINTER;
+
+ boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0);
+ if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) {
+ // If we're in an IDLE state to begin with and aren't moving anywhere, we
+ // end up having a non-null capturedView with an IDLE dragState
+ mCapturedView = null;
+ }
+
+ return continueSliding;
+ }
+
+ /**
+ * Settle the captured view at the given (left, top) position. The appropriate velocity from prior motion will be
+ * taken into account. If this method returns true, the caller should invoke {@link #continueSettling(boolean)} on
+ * each subsequent frame to continue the motion until it returns false. If this method returns false there is no
+ * further work to do to complete the movement.
+ *
+ * @param finalLeft
+ * Settled left edge position for the captured view
+ * @param finalTop
+ * Settled top edge position for the captured view
+ * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+ */
+ public boolean settleCapturedViewAt(int finalLeft, int finalTop) {
+ if (!mReleaseInProgress) {
+ throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + "Callback#onViewReleased");
+ }
+
+ return forceSettleCapturedViewAt(finalLeft, finalTop, (int) mVelocityTracker.getXVelocity(mActivePointerId),
+ (int) mVelocityTracker.getYVelocity(mActivePointerId));
+ }
+
+ /**
+ * Settle the captured view at the given (left, top) position.
+ *
+ * @param finalLeft
+ * Target left position for the captured view
+ * @param finalTop
+ * Target top position for the captured view
+ * @param xvel
+ * Horizontal velocity
+ * @param yvel
+ * Vertical velocity
+ * @return true if animation should continue through {@link #continueSettling(boolean)} calls
+ */
+ private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) {
+ final int startLeft = mCapturedView.getLeft();
+ final int startTop = mCapturedView.getTop();
+ final int dx = finalLeft - startLeft;
+ final int dy = finalTop - startTop;
+
+ if (dx == 0 && dy == 0) {
+ // Nothing to do. Send callbacks, be done.
+ mScroller.abortAnimation();
+ setDragState(STATE_IDLE);
+ return false;
+ }
+
+ final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel);
+ mScroller.startScroll(startLeft, startTop, dx, dy, duration);
+
+ setDragState(STATE_SETTLING);
+ return true;
+ }
+
+ private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) {
+ xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity);
+ yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity);
+ final int absDx = Math.abs(dx);
+ final int absDy = Math.abs(dy);
+ final int absXVel = Math.abs(xvel);
+ final int absYVel = Math.abs(yvel);
+ final int addedVel = absXVel + absYVel;
+ final int addedDistance = absDx + absDy;
+
+ final float xweight = xvel != 0 ? (float) absXVel / addedVel : (float) absDx / addedDistance;
+ final float yweight = yvel != 0 ? (float) absYVel / addedVel : (float) absDy / addedDistance;
+
+ int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child));
+ int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child));
+
+ return (int) (xduration * xweight + yduration * yweight);
+ }
+
+ private int computeAxisDuration(int delta, int velocity, int motionRange) {
+ if (delta == 0) {
+ return 0;
+ }
+
+ final int width = mParentView.getWidth();
+ final int halfWidth = width / 2;
+ final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width);
+ final float distance = halfWidth + halfWidth * distanceInfluenceForSnapDuration(distanceRatio);
+
+ int duration;
+ velocity = Math.abs(velocity);
+ if (velocity > 0) {
+ duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+ } else {
+ final float range = (float) Math.abs(delta) / motionRange;
+ duration = (int) ((range + 1) * BASE_SETTLE_DURATION);
+ }
+ return Math.min(duration, MAX_SETTLE_DURATION);
+ }
+
+ /**
+ * Clamp the magnitude of value for absMin and absMax. If the value is below the minimum, it will be clamped to
+ * zero. If the value is above the maximum, it will be clamped to the maximum.
+ *
+ * @param value
+ * Value to clamp
+ * @param absMin
+ * Absolute value of the minimum significant value to return
+ * @param absMax
+ * Absolute value of the maximum value to return
+ * @return The clamped value with the same sign as value
+ */
+ private static int clampMag(int value, int absMin, int absMax) {
+ final int absValue = Math.abs(value);
+ if (absValue < absMin) return 0;
+ if (absValue > absMax) return value > 0 ? absMax : -absMax;
+ return value;
+ }
+
+ /**
+ * Clamp the magnitude of value for absMin and absMax. If the value is below the minimum, it will be clamped to
+ * zero. If the value is above the maximum, it will be clamped to the maximum.
+ *
+ * @param value
+ * Value to clamp
+ * @param absMin
+ * Absolute value of the minimum significant value to return
+ * @param absMax
+ * Absolute value of the maximum value to return
+ * @return The clamped value with the same sign as value
+ */
+ private static float clampMag(float value, float absMin, float absMax) {
+ final float absValue = Math.abs(value);
+ if (absValue < absMin) return 0;
+ if (absValue > absMax) return value > 0 ? absMax : -absMax;
+ return value;
+ }
+
+ private static float distanceInfluenceForSnapDuration(float f) {
+ f -= 0.5f; // center the values about 0.
+ f *= 0.3f * Math.PI / 2.0f;
+ return (float) Math.sin(f);
+ }
+
+ /**
+ * Settle the captured view based on standard free-moving fling behavior. The caller should invoke
+ * {@link #continueSettling(boolean)} on each subsequent frame to continue the motion until it returns false.
+ *
+ * @param minLeft
+ * Minimum X position for the view's left edge
+ * @param minTop
+ * Minimum Y position for the view's top edge
+ * @param maxLeft
+ * Maximum X position for the view's left edge
+ * @param maxTop
+ * Maximum Y position for the view's top edge
+ */
+ public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) {
+ if (!mReleaseInProgress) {
+ throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + "Callback#onViewReleased");
+ }
+
+ mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), (int) mVelocityTracker.getXVelocity(mActivePointerId),
+ (int) mVelocityTracker.getYVelocity(mActivePointerId), minLeft, maxLeft, minTop, maxTop);
+
+ setDragState(STATE_SETTLING);
+ }
+
+ /**
+ * Move the captured settling view by the appropriate amount for the current time. If continueSettling
+ * returns true, the caller should call it again on the next frame to continue.
+ *
+ * @param deferCallbacks
+ * true if state callbacks should be deferred via posted message. Set this to true if you are calling
+ * this method from {@link android.view.View#computeScroll()} or similar methods invoked as part of
+ * layout or drawing.
+ * @return true if settle is still in progress
+ */
+ public boolean continueSettling(boolean deferCallbacks) {
+ if (mDragState == STATE_SETTLING) {
+ boolean keepGoing = mScroller.computeScrollOffset();
+ final int x = mScroller.getCurrX();
+ final int y = mScroller.getCurrY();
+ final int dx = x - mCapturedView.getLeft();
+ final int dy = y - mCapturedView.getTop();
+
+ if (dx != 0) {
+ mCapturedView.offsetLeftAndRight(dx);
+ }
+ if (dy != 0) {
+ mCapturedView.offsetTopAndBottom(dy);
+ }
+
+ if (dx != 0 || dy != 0) {
+ mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
+ }
+
+ if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
+ // Close enough. The interpolator/scroller might think we're still moving
+ // but the user sure doesn't.
+ mScroller.abortAnimation();
+ keepGoing = false;
+ }
+
+ if (!keepGoing) {
+ if (deferCallbacks) {
+ mParentView.post(mSetIdleRunnable);
+ } else {
+ setDragState(STATE_IDLE);
+ }
+ }
+ }
+
+ return mDragState == STATE_SETTLING;
+ }
+
+ /**
+ * Like all callback events this must happen on the UI thread, but release involves some extra semantics. During a
+ * release (mReleaseInProgress) is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} or
+ * {@link #flingCapturedView(int, int, int, int)}.
+ */
+ private void dispatchViewReleased(float xvel, float yvel) {
+ mReleaseInProgress = true;
+ mCallback.onViewReleased(mCapturedView, xvel, yvel);
+ mReleaseInProgress = false;
+
+ if (mDragState == STATE_DRAGGING) {
+ // onViewReleased didn't call a method that would have changed this. Go idle.
+ setDragState(STATE_IDLE);
+ }
+ }
+
+ private void clearMotionHistory() {
+ if (mInitialMotionX == null) {
+ return;
+ }
+ Arrays.fill(mInitialMotionX, 0);
+ Arrays.fill(mInitialMotionY, 0);
+ Arrays.fill(mLastMotionX, 0);
+ Arrays.fill(mLastMotionY, 0);
+ Arrays.fill(mInitialEdgesTouched, 0);
+ Arrays.fill(mEdgeDragsInProgress, 0);
+ Arrays.fill(mEdgeDragsLocked, 0);
+ mPointersDown = 0;
+ }
+
+ private void clearMotionHistory(int pointerId) {
+ if (mInitialMotionX == null) {
+ return;
+ }
+ mInitialMotionX[pointerId] = 0;
+ mInitialMotionY[pointerId] = 0;
+ mLastMotionX[pointerId] = 0;
+ mLastMotionY[pointerId] = 0;
+ mInitialEdgesTouched[pointerId] = 0;
+ mEdgeDragsInProgress[pointerId] = 0;
+ mEdgeDragsLocked[pointerId] = 0;
+ mPointersDown &= ~(1 << pointerId);
+ }
+
+ private void ensureMotionHistorySizeForId(int pointerId) {
+ if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) {
+ float[] imx = new float[pointerId + 1];
+ float[] imy = new float[pointerId + 1];
+ float[] lmx = new float[pointerId + 1];
+ float[] lmy = new float[pointerId + 1];
+ int[] iit = new int[pointerId + 1];
+ int[] edip = new int[pointerId + 1];
+ int[] edl = new int[pointerId + 1];
+
+ if (mInitialMotionX != null) {
+ System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length);
+ System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length);
+ System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length);
+ System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length);
+ System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length);
+ System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length);
+ System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length);
+ }
+
+ mInitialMotionX = imx;
+ mInitialMotionY = imy;
+ mLastMotionX = lmx;
+ mLastMotionY = lmy;
+ mInitialEdgesTouched = iit;
+ mEdgeDragsInProgress = edip;
+ mEdgeDragsLocked = edl;
+ }
+ }
+
+ private void saveInitialMotion(float x, float y, int pointerId) {
+ ensureMotionHistorySizeForId(pointerId);
+ mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x;
+ mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y;
+ mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y);
+ mPointersDown |= 1 << pointerId;
+ }
+
+ private void saveLastMotion(MotionEvent ev) {
+ final int pointerCount = ev.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = ev.getPointerId(i);
+ final float x = ev.getX(i);
+ final float y = ev.getY(i);
+ mLastMotionX[pointerId] = x;
+ mLastMotionY[pointerId] = y;
+ }
+ }
+
+ /**
+ * Check if the given pointer ID represents a pointer that is currently down (to the best of the ViewDragHelper's
+ * knowledge).
+ *
+ *
+ * The state used to report this information is populated by the methods
+ * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+ * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not been called for all
+ * relevant MotionEvents to track, the information reported by this method may be stale or incorrect.
+ *
+ *
+ * @param pointerId
+ * pointer ID to check; corresponds to IDs provided by MotionEvent
+ * @return true if the pointer with the given ID is still down
+ */
+ public boolean isPointerDown(int pointerId) {
+ return (mPointersDown & 1 << pointerId) != 0;
+ }
+
+ void setDragState(int state) {
+ mParentView.removeCallbacks(mSetIdleRunnable);
+ if (mDragState != state) {
+ mDragState = state;
+ mCallback.onViewDragStateChanged(state);
+ if (mDragState == STATE_IDLE) {
+ mCapturedView = null;
+ }
+ }
+ }
+
+ /**
+ * Attempt to capture the view with the given pointer ID. The callback will be involved. This will put us into the
+ * "dragging" state. If we've already captured this view with this pointer this method will immediately return true
+ * without consulting the callback.
+ *
+ * @param toCapture
+ * View to capture
+ * @param pointerId
+ * Pointer to capture with
+ * @return true if capture was successful
+ */
+ boolean tryCaptureViewForDrag(View toCapture, int pointerId) {
+ if (toCapture == mCapturedView && mActivePointerId == pointerId) {
+ // Already done!
+ return true;
+ }
+ if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) {
+ mActivePointerId = pointerId;
+ captureChildView(toCapture, pointerId);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Tests scrollability within child views of v given a delta of dx.
+ *
+ * @param v
+ * View to test for horizontal scrollability
+ * @param checkV
+ * Whether the view v passed should itself be checked for scrollability (true), or just its children
+ * (false).
+ * @param dx
+ * Delta scrolled in pixels along the X axis
+ * @param dy
+ * Delta scrolled in pixels along the Y axis
+ * @param x
+ * X coordinate of the active touch point
+ * @param y
+ * Y coordinate of the active touch point
+ * @return true if child views of v can be scrolled by delta of dx.
+ */
+ protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) {
+ if (v instanceof ViewGroup) {
+ final ViewGroup group = (ViewGroup) v;
+ final int scrollX = v.getScrollX();
+ final int scrollY = v.getScrollY();
+ final int count = group.getChildCount();
+ // Count backwards - let topmost views consume scroll distance first.
+ for (int i = count - 1; i >= 0; i--) {
+ // TODO: Add versioned support here for transformed views.
+ // This will not work for transformed views in Honeycomb+
+ final View child = group.getChildAt(i);
+ if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && y + scrollY >= child.getTop() && y + scrollY < child.getBottom()
+ && canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), y + scrollY - child.getTop())) {
+ return true;
+ }
+ }
+ }
+
+ return checkV && (v.canScrollHorizontally(-dx) || v.canScrollVertically(-dy));
+ }
+
+ /**
+ * Check if this event as provided to the parent view's onInterceptTouchEvent should cause the parent to intercept
+ * the touch event stream.
+ *
+ * @param ev
+ * MotionEvent provided to onInterceptTouchEvent
+ * @return true if the parent view should return true from onInterceptTouchEvent
+ */
+ public boolean shouldInterceptTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+ final int actionIndex = ev.getActionIndex();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Reset things for a new event stream, just in case we didn't get
+ // the whole previous stream.
+ cancel();
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final int pointerId = ev.getPointerId(0);
+ saveInitialMotion(x, y, pointerId);
+
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+
+ // Catch a settling view if possible.
+ if (toCapture == mCapturedView && mDragState == STATE_SETTLING) {
+ tryCaptureViewForDrag(toCapture, pointerId);
+ }
+
+ final int edgesTouched = mInitialEdgesTouched[pointerId];
+ if ((edgesTouched & mTrackingEdges) != 0) {
+ mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerId = ev.getPointerId(actionIndex);
+ final float x = ev.getX(actionIndex);
+ final float y = ev.getY(actionIndex);
+
+ saveInitialMotion(x, y, pointerId);
+
+ // A ViewDragHelper can only manipulate one view at a time.
+ if (mDragState == STATE_IDLE) {
+ final int edgesTouched = mInitialEdgesTouched[pointerId];
+ if ((edgesTouched & mTrackingEdges) != 0) {
+ mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+ }
+ } else if (mDragState == STATE_SETTLING) {
+ // Catch a settling view if possible.
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+ if (toCapture == mCapturedView) {
+ tryCaptureViewForDrag(toCapture, pointerId);
+ }
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ // First to cross a touch slop over a draggable view wins. Also report edge drags.
+ final int pointerCount = ev.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = ev.getPointerId(i);
+ final float x = ev.getX(i);
+ final float y = ev.getY(i);
+ final float dx = x - mInitialMotionX[pointerId];
+ final float dy = y - mInitialMotionY[pointerId];
+
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+ final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy);
+ if (pastSlop) {
+ // check the callback's
+ // getView[Horizontal|Vertical]DragRange methods to know
+ // if you can move at all along an axis, then see if it
+ // would clamp to the same value. If you can't move at
+ // all in every dimension with a nonzero range, bail.
+ @SuppressWarnings("null") /* We only enter here if "toCapture != null" */
+ final int oldLeft = toCapture.getLeft();
+ final int targetLeft = oldLeft + (int) dx;
+ final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, targetLeft, (int) dx);
+ final int oldTop = toCapture.getTop();
+ final int targetTop = oldTop + (int) dy;
+ final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, (int) dy);
+ final int horizontalDragRange = mCallback.getViewHorizontalDragRange(toCapture);
+ final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture);
+ if ((horizontalDragRange == 0 || horizontalDragRange > 0 && newLeft == oldLeft)
+ && (verticalDragRange == 0 || verticalDragRange > 0 && newTop == oldTop)) {
+ break;
+ }
+ }
+ reportNewEdgeDrags(dx, dy, pointerId);
+ if (mDragState == STATE_DRAGGING) {
+ // Callback might have started an edge drag
+ break;
+ }
+
+ if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) {
+ break;
+ }
+ }
+ saveLastMotion(ev);
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ final int pointerId = ev.getPointerId(actionIndex);
+ clearMotionHistory(pointerId);
+ break;
+ }
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL: {
+ cancel();
+ break;
+ }
+ }
+
+ return mDragState == STATE_DRAGGING;
+ }
+
+ /**
+ * Process a touch event received by the parent view. This method will dispatch callback events as needed before
+ * returning. The parent view's onTouchEvent implementation should call this.
+ *
+ * @param ev
+ * The touch event received by the parent view
+ */
+ public void processTouchEvent(MotionEvent ev) {
+ final int action = ev.getActionMasked();
+ final int actionIndex = ev.getActionIndex();
+
+ if (action == MotionEvent.ACTION_DOWN) {
+ // Reset things for a new event stream, just in case we didn't get
+ // the whole previous stream.
+ cancel();
+ }
+
+ if (mVelocityTracker == null) {
+ mVelocityTracker = VelocityTracker.obtain();
+ }
+ mVelocityTracker.addMovement(ev);
+
+ switch (action) {
+ case MotionEvent.ACTION_DOWN: {
+ final float x = ev.getX();
+ final float y = ev.getY();
+ final int pointerId = ev.getPointerId(0);
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+
+ saveInitialMotion(x, y, pointerId);
+
+ // Since the parent is already directly processing this touch event,
+ // there is no reason to delay for a slop before dragging.
+ // Start immediately if possible.
+ tryCaptureViewForDrag(toCapture, pointerId);
+
+ final int edgesTouched = mInitialEdgesTouched[pointerId];
+ if ((edgesTouched & mTrackingEdges) != 0) {
+ mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_DOWN: {
+ final int pointerId = ev.getPointerId(actionIndex);
+ final float x = ev.getX(actionIndex);
+ final float y = ev.getY(actionIndex);
+
+ saveInitialMotion(x, y, pointerId);
+
+ // A ViewDragHelper can only manipulate one view at a time.
+ if (mDragState == STATE_IDLE) {
+ // If we're idle we can do anything! Treat it like a normal down event.
+
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+ tryCaptureViewForDrag(toCapture, pointerId);
+
+ final int edgesTouched = mInitialEdgesTouched[pointerId];
+ if ((edgesTouched & mTrackingEdges) != 0) {
+ mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId);
+ }
+ } else if (isCapturedViewUnder((int) x, (int) y)) {
+ // We're still tracking a captured view. If the same view is under this
+ // point, we'll swap to controlling it with this pointer instead.
+ // (This will still work if we're "catching" a settling view.)
+
+ tryCaptureViewForDrag(mCapturedView, pointerId);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_MOVE: {
+ if (mDragState == STATE_DRAGGING) {
+ final int index = ev.findPointerIndex(mActivePointerId);
+ final float x = ev.getX(index);
+ final float y = ev.getY(index);
+ final int idx = (int) (x - mLastMotionX[mActivePointerId]);
+ final int idy = (int) (y - mLastMotionY[mActivePointerId]);
+
+ dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy);
+
+ saveLastMotion(ev);
+ } else {
+ // Check to see if any pointer is now over a draggable view.
+ final int pointerCount = ev.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int pointerId = ev.getPointerId(i);
+ final float x = ev.getX(i);
+ final float y = ev.getY(i);
+ final float dx = x - mInitialMotionX[pointerId];
+ final float dy = y - mInitialMotionY[pointerId];
+
+ reportNewEdgeDrags(dx, dy, pointerId);
+ if (mDragState == STATE_DRAGGING) {
+ // Callback might have started an edge drag.
+ break;
+ }
+
+ final View toCapture = findTopChildUnder((int) x, (int) y);
+ if (checkTouchSlop(toCapture, dx, dy) && tryCaptureViewForDrag(toCapture, pointerId)) {
+ break;
+ }
+ }
+ saveLastMotion(ev);
+ }
+ break;
+ }
+
+ case MotionEvent.ACTION_POINTER_UP: {
+ final int pointerId = ev.getPointerId(actionIndex);
+ if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) {
+ // Try to find another pointer that's still holding on to the captured view.
+ int newActivePointer = INVALID_POINTER;
+ final int pointerCount = ev.getPointerCount();
+ for (int i = 0; i < pointerCount; i++) {
+ final int id = ev.getPointerId(i);
+ if (id == mActivePointerId) {
+ // This one's going away, skip.
+ continue;
+ }
+
+ final float x = ev.getX(i);
+ final float y = ev.getY(i);
+ if (findTopChildUnder((int) x, (int) y) == mCapturedView && tryCaptureViewForDrag(mCapturedView, id)) {
+ newActivePointer = mActivePointerId;
+ break;
+ }
+ }
+
+ if (newActivePointer == INVALID_POINTER) {
+ // We didn't find another pointer still touching the view, release it.
+ releaseViewForPointerUp();
+ }
+ }
+ clearMotionHistory(pointerId);
+ break;
+ }
+
+ case MotionEvent.ACTION_UP: {
+ if (mDragState == STATE_DRAGGING) {
+ releaseViewForPointerUp();
+ }
+ cancel();
+ break;
+ }
+
+ case MotionEvent.ACTION_CANCEL: {
+ if (mDragState == STATE_DRAGGING) {
+ dispatchViewReleased(0, 0);
+ }
+ cancel();
+ break;
+ }
+ }
+ }
+
+ private void reportNewEdgeDrags(float dx, float dy, int pointerId) {
+ int dragsStarted = 0;
+ if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) {
+ dragsStarted |= EDGE_LEFT;
+ }
+ if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) {
+ dragsStarted |= EDGE_TOP;
+ }
+ if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) {
+ dragsStarted |= EDGE_RIGHT;
+ }
+ if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) {
+ dragsStarted |= EDGE_BOTTOM;
+ }
+
+ if (dragsStarted != 0) {
+ mEdgeDragsInProgress[pointerId] |= dragsStarted;
+ mCallback.onEdgeDragStarted(dragsStarted, pointerId);
+ }
+ }
+
+ private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) {
+ final float absDelta = Math.abs(delta);
+ final float absODelta = Math.abs(odelta);
+
+ if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || (mEdgeDragsLocked[pointerId] & edge) == edge
+ || (mEdgeDragsInProgress[pointerId] & edge) == edge || (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) {
+ return false;
+ }
+ if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) {
+ mEdgeDragsLocked[pointerId] |= edge;
+ return false;
+ }
+ return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop;
+ }
+
+ /**
+ * Check if we've crossed a reasonable touch slop for the given child view. If the child cannot be dragged along the
+ * horizontal or vertical axis, motion along that axis will not count toward the slop check.
+ *
+ * @param child
+ * Child to check
+ * @param dx
+ * Motion since initial position along X axis
+ * @param dy
+ * Motion since initial position along Y axis
+ * @return true if the touch slop has been crossed
+ */
+ private boolean checkTouchSlop(View child, float dx, float dy) {
+ if (child == null) {
+ return false;
+ }
+ final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0;
+ final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0;
+
+ if (checkHorizontal && checkVertical) {
+ return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
+ } else if (checkHorizontal) {
+ return Math.abs(dx) > mTouchSlop;
+ } else if (checkVertical) {
+ return Math.abs(dy) > mTouchSlop;
+ }
+ return false;
+ }
+
+ /**
+ * Check if any pointer tracked in the current gesture has crossed the required slop threshold.
+ *
+ *
+ * This depends on internal state populated by {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+ * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on the results of this method after
+ * all currently available touch data has been provided to one of these two methods.
+ *
+ *
+ * @param directions
+ * Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL},
+ * {@link #DIRECTION_ALL}
+ * @return true if the slop threshold has been crossed, false otherwise
+ */
+ public boolean checkTouchSlop(int directions) {
+ final int count = mInitialMotionX.length;
+ for (int i = 0; i < count; i++) {
+ if (checkTouchSlop(directions, i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if the specified pointer tracked in the current gesture has crossed the required slop threshold.
+ *
+ *
+ * This depends on internal state populated by {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or
+ * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on the results of this method after
+ * all currently available touch data has been provided to one of these two methods.
+ *
+ *
+ * @param directions
+ * Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, {@link #DIRECTION_VERTICAL},
+ * {@link #DIRECTION_ALL}
+ * @param pointerId
+ * ID of the pointer to slop check as specified by MotionEvent
+ * @return true if the slop threshold has been crossed, false otherwise
+ */
+ public boolean checkTouchSlop(int directions, int pointerId) {
+ if (!isPointerDown(pointerId)) {
+ return false;
+ }
+
+ final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL;
+ final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL;
+
+ final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId];
+ final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId];
+
+ if (checkHorizontal && checkVertical) {
+ return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
+ } else if (checkHorizontal) {
+ return Math.abs(dx) > mTouchSlop;
+ } else if (checkVertical) {
+ return Math.abs(dy) > mTouchSlop;
+ }
+ return false;
+ }
+
+ /**
+ * Check if any of the edges specified were initially touched in the currently active gesture. If there is no
+ * currently active gesture this method will return false.
+ *
+ * @param edges
+ * Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, {@link #EDGE_TOP},
+ * {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and {@link #EDGE_ALL}
+ * @return true if any of the edges specified were initially touched in the current gesture
+ */
+ public boolean isEdgeTouched(int edges) {
+ final int count = mInitialEdgesTouched.length;
+ for (int i = 0; i < count; i++) {
+ if (isEdgeTouched(edges, i)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Check if any of the edges specified were initially touched by the pointer with the specified ID. If there is no
+ * currently active gesture or if there is no pointer with the given ID currently down this method will return
+ * false.
+ *
+ * @param edges
+ * Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, {@link #EDGE_TOP},
+ * {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and {@link #EDGE_ALL}
+ * @return true if any of the edges specified were initially touched in the current gesture
+ */
+ public boolean isEdgeTouched(int edges, int pointerId) {
+ return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0;
+ }
+
+ private void releaseViewForPointerUp() {
+ mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity);
+ final float xvel = clampMag(mVelocityTracker.getXVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
+ final float yvel = clampMag(mVelocityTracker.getYVelocity(mActivePointerId), mMinVelocity, mMaxVelocity);
+ dispatchViewReleased(xvel, yvel);
+ }
+
+ private void dragTo(int left, int top, int dx, int dy) {
+ int clampedX = left;
+ int clampedY = top;
+ final int oldLeft = mCapturedView.getLeft();
+ final int oldTop = mCapturedView.getTop();
+ if (dx != 0) {
+ clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx);
+ mCapturedView.offsetLeftAndRight(clampedX - oldLeft);
+ }
+ if (dy != 0) {
+ clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy);
+ mCapturedView.offsetTopAndBottom(clampedY - oldTop);
+ }
+
+ if (dx != 0 || dy != 0) {
+ final int clampedDx = clampedX - oldLeft;
+ final int clampedDy = clampedY - oldTop;
+ mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, clampedDx, clampedDy);
+ }
+ }
+
+ /**
+ * Determine if the currently captured view is under the given point in the parent view's coordinate system. If
+ * there is no captured view this method will return false.
+ *
+ * @param x
+ * X position to test in the parent's coordinate system
+ * @param y
+ * Y position to test in the parent's coordinate system
+ * @return true if the captured view is under the given point, false otherwise
+ */
+ public boolean isCapturedViewUnder(int x, int y) {
+ return isViewUnder(mCapturedView, x, y);
+ }
+
+ /**
+ * Determine if the supplied view is under the given point in the parent view's coordinate system.
+ *
+ * @param view
+ * Child view of the parent to hit test
+ * @param x
+ * X position to test in the parent's coordinate system
+ * @param y
+ * Y position to test in the parent's coordinate system
+ * @return true if the supplied view is under the given point, false otherwise
+ */
+ public boolean isViewUnder(View view, int x, int y) {
+ if (view == null) {
+ return false;
+ }
+ return x >= view.getLeft() && x < view.getRight() && y >= view.getTop() && y < view.getBottom();
+ }
+
+ /**
+ * Find the topmost child under the given point within the parent view's coordinate system. The child order is
+ * determined using {@link Callback#getOrderedChildIndex(int)}.
+ *
+ * @param x
+ * X position to test in the parent's coordinate system
+ * @param y
+ * Y position to test in the parent's coordinate system
+ * @return The topmost child view under (x, y) or null if none found.
+ */
+ public View findTopChildUnder(int x, int y) {
+ final int childCount = mParentView.getChildCount();
+ for (int i = childCount - 1; i >= 0; i--) {
+ final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i));
+ if (x >= child.getLeft() && x < child.getRight() && y >= child.getTop() && y < child.getBottom()) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ private int getEdgesTouched(int x, int y) {
+ int result = 0;
+
+ if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT;
+ if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP;
+ if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT;
+ if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM;
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/termux/drawer/package-info.java b/app/src/main/java/com/termux/drawer/package-info.java
new file mode 100644
index 0000000000..84bf92f77c
--- /dev/null
+++ b/app/src/main/java/com/termux/drawer/package-info.java
@@ -0,0 +1,10 @@
+/**
+ * Extraction (and some minor cleanup to get rid of warnings) of DrawerLayout from the
+ * Android Support Library.
+ *
+ * Source at:
+ * https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/DrawerLayout.java
+ * https://android.googlesource.com/platform/frameworks/support/+/refs/heads/master/v4/java/android/support/v4/widget/ViewDragHelper.java
+ */
+package com.termux.drawer;
+
diff --git a/app/src/main/java/com/termux/terminal/ByteQueue.java b/app/src/main/java/com/termux/terminal/ByteQueue.java
new file mode 100644
index 0000000000..1eb3e59def
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/ByteQueue.java
@@ -0,0 +1,108 @@
+package com.termux.terminal;
+
+/** A circular byte buffer allowing one producer and one consumer thread. */
+final class ByteQueue {
+
+ private final byte[] mBuffer;
+ private int mHead;
+ private int mStoredBytes;
+ private boolean mOpen = true;
+
+ public ByteQueue(int size) {
+ mBuffer = new byte[size];
+ }
+
+ public synchronized void close() {
+ mOpen = false;
+ notify();
+ }
+
+ public synchronized int read(byte[] buffer, boolean block) {
+ while (mStoredBytes == 0 && mOpen) {
+ if (block) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ } else {
+ return 0;
+ }
+ }
+ if (!mOpen) return -1;
+
+ int totalRead = 0;
+ int bufferLength = mBuffer.length;
+ boolean wasFull = bufferLength == mStoredBytes;
+ int length = buffer.length;
+ int offset = 0;
+ while (length > 0 && mStoredBytes > 0) {
+ int oneRun = Math.min(bufferLength - mHead, mStoredBytes);
+ int bytesToCopy = Math.min(length, oneRun);
+ System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy);
+ mHead += bytesToCopy;
+ if (mHead >= bufferLength) mHead = 0;
+ mStoredBytes -= bytesToCopy;
+ length -= bytesToCopy;
+ offset += bytesToCopy;
+ totalRead += bytesToCopy;
+ }
+ if (wasFull) notify();
+ return totalRead;
+ }
+
+ /**
+ * Attempt to write the specified portion of the provided buffer to the queue.
+ *
+ * Returns whether the output was totally written, false if it was closed before.
+ */
+ public boolean write(byte[] buffer, int offset, int lengthToWrite) {
+ if (lengthToWrite + offset > buffer.length) {
+ throw new IllegalArgumentException("length + offset > buffer.length");
+ } else if (lengthToWrite <= 0) {
+ throw new IllegalArgumentException("length <= 0");
+ }
+
+ final int bufferLength = mBuffer.length;
+
+ synchronized (this) {
+ while (lengthToWrite > 0) {
+ while (bufferLength == mStoredBytes && mOpen) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ // Ignore.
+ }
+ }
+ if (!mOpen) return false;
+ final boolean wasEmpty = mStoredBytes == 0;
+ int bytesToWriteBeforeWaiting = Math.min(lengthToWrite, bufferLength - mStoredBytes);
+ lengthToWrite -= bytesToWriteBeforeWaiting;
+
+ while (bytesToWriteBeforeWaiting > 0) {
+ int tail = mHead + mStoredBytes;
+ int oneRun;
+ if (tail >= bufferLength) {
+ // Buffer: [.............]
+ // ________________H_______T
+ // =>
+ // Buffer: [.............]
+ // ___________T____H
+ // onRun= _____----_
+ tail = tail - bufferLength;
+ oneRun = mHead - tail;
+ } else {
+ oneRun = bufferLength - tail;
+ }
+ int bytesToCopy = Math.min(oneRun, bytesToWriteBeforeWaiting);
+ System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy);
+ offset += bytesToCopy;
+ bytesToWriteBeforeWaiting -= bytesToCopy;
+ mStoredBytes += bytesToCopy;
+ }
+ if (wasEmpty) notify();
+ }
+ }
+ return true;
+ }
+}
diff --git a/app/src/main/java/com/termux/terminal/EmulatorDebug.java b/app/src/main/java/com/termux/terminal/EmulatorDebug.java
new file mode 100644
index 0000000000..213657a92c
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/EmulatorDebug.java
@@ -0,0 +1,10 @@
+package com.termux.terminal;
+
+import android.util.Log;
+
+public final class EmulatorDebug {
+
+ /** The tag to use with {@link Log}. */
+ public static final String LOG_TAG = "termux";
+
+}
diff --git a/app/src/main/java/com/termux/terminal/JNI.java b/app/src/main/java/com/termux/terminal/JNI.java
new file mode 100644
index 0000000000..423541a6a7
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/JNI.java
@@ -0,0 +1,55 @@
+package com.termux.terminal;
+
+/**
+ * Native methods for creating and managing pseudoterminal subprocesses. C code is in jni/termux.c.
+ */
+final class JNI {
+
+ static {
+ System.loadLibrary("termux");
+ }
+
+ /**
+ * Create a subprocess. Differs from {@link ProcessBuilder} in that a pseudoterminal is used to communicate with the
+ * subprocess.
+ *
+ * Callers are responsible for calling {@link #close(int)} on the returned file descriptor.
+ *
+ * @param cmd
+ * The command to execute
+ * @param cwd
+ * The current working directory for the executed command
+ * @param args
+ * An array of arguments to the command
+ * @param envVars
+ * An array of strings of the form "VAR=value" to be added to the environment of the process
+ * @param processId
+ * A one-element array to which the process ID of the started process will be written.
+ * @return the file descriptor resulting from opening /dev/ptmx master device. The sub process will have opened the
+ * slave device counterpart (/dev/pts/$N) and have it as stdint, stdout and stderr.
+ */
+ public static native int createSubprocess(String cmd, String cwd, String[] args, String[] envVars, int[] processId);
+
+ /** Set the window size for a given pty, which allows connected programs to learn how large their screen is. */
+ public static native void setPtyWindowSize(int fd, int rows, int cols);
+
+ /**
+ * Causes the calling thread to wait for the process associated with the receiver to finish executing.
+ *
+ * @return if >= 0, the exit status of the process. If < 0, the signal causing the process to stop negated.
+ */
+ public static native int waitFor(int processId);
+
+ /**
+ * Send SIGHUP to a process group.
+ *
+ * There exists a kill(2) system call wrapper in {@link android.os.Process#sendSignal(int, int)}, but that makes a
+ * "if (pid > 0)" check so cannot be used for sending to a process group:
+ * https://android.googlesource.com/platform/frameworks/base/+/donut-release/core/jni/android_util_Process.cpp
+ */
+ public static native void hangupProcessGroup(int processId);
+
+ /** Close a file descriptor through the close(2) system call. */
+ public static native void close(int fileDescriptor);
+
+}
diff --git a/app/src/main/java/com/termux/terminal/KeyHandler.java b/app/src/main/java/com/termux/terminal/KeyHandler.java
new file mode 100644
index 0000000000..0145137652
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/KeyHandler.java
@@ -0,0 +1,310 @@
+package com.termux.terminal;
+
+import static android.view.KeyEvent.KEYCODE_BREAK;
+import static android.view.KeyEvent.KEYCODE_DEL;
+import static android.view.KeyEvent.KEYCODE_DPAD_CENTER;
+import static android.view.KeyEvent.KEYCODE_DPAD_DOWN;
+import static android.view.KeyEvent.KEYCODE_DPAD_LEFT;
+import static android.view.KeyEvent.KEYCODE_DPAD_RIGHT;
+import static android.view.KeyEvent.KEYCODE_DPAD_UP;
+import static android.view.KeyEvent.KEYCODE_ENTER;
+import static android.view.KeyEvent.KEYCODE_ESCAPE;
+import static android.view.KeyEvent.KEYCODE_F1;
+import static android.view.KeyEvent.KEYCODE_F10;
+import static android.view.KeyEvent.KEYCODE_F11;
+import static android.view.KeyEvent.KEYCODE_F12;
+import static android.view.KeyEvent.KEYCODE_F2;
+import static android.view.KeyEvent.KEYCODE_F3;
+import static android.view.KeyEvent.KEYCODE_F4;
+import static android.view.KeyEvent.KEYCODE_F5;
+import static android.view.KeyEvent.KEYCODE_F6;
+import static android.view.KeyEvent.KEYCODE_F7;
+import static android.view.KeyEvent.KEYCODE_F8;
+import static android.view.KeyEvent.KEYCODE_F9;
+import static android.view.KeyEvent.KEYCODE_FORWARD_DEL;
+import static android.view.KeyEvent.KEYCODE_INSERT;
+import static android.view.KeyEvent.KEYCODE_MOVE_END;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_0;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_1;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_2;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_3;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_4;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_5;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_6;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_7;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_8;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_9;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_ADD;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_COMMA;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_DIVIDE;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_DOT;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_ENTER;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_EQUALS;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_MULTIPLY;
+import static android.view.KeyEvent.KEYCODE_NUMPAD_SUBTRACT;
+import static android.view.KeyEvent.KEYCODE_NUM_LOCK;
+import static android.view.KeyEvent.KEYCODE_PAGE_DOWN;
+import static android.view.KeyEvent.KEYCODE_PAGE_UP;
+import static android.view.KeyEvent.KEYCODE_SYSRQ;
+import static android.view.KeyEvent.KEYCODE_TAB;
+import static android.view.KeyEvent.KEYCODE_HOME;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import android.view.KeyEvent;
+
+public final class KeyHandler {
+
+ public static final int KEYMOD_ALT = 0x80000000;
+ public static final int KEYMOD_CTRL = 0x40000000;
+ public static final int KEYMOD_SHIFT = 0x20000000;
+
+ private static final Map TERMCAP_TO_KEYCODE = new HashMap<>();
+ static {
+ // terminfo: http://pubs.opengroup.org/onlinepubs/7990989799/xcurses/terminfo.html
+ // termcap: http://man7.org/linux/man-pages/man5/termcap.5.html
+ TERMCAP_TO_KEYCODE.put("%i", KEYMOD_SHIFT | KEYCODE_DPAD_RIGHT);
+ TERMCAP_TO_KEYCODE.put("#2", KEYMOD_SHIFT | KEYCODE_HOME); // Shifted home
+ TERMCAP_TO_KEYCODE.put("#4", KEYMOD_SHIFT | KEYCODE_DPAD_LEFT);
+ TERMCAP_TO_KEYCODE.put("*7", KEYMOD_SHIFT | KEYCODE_MOVE_END); // Shifted end key
+
+ TERMCAP_TO_KEYCODE.put("k1", KEYCODE_F1);
+ TERMCAP_TO_KEYCODE.put("k2", KEYCODE_F2);
+ TERMCAP_TO_KEYCODE.put("k3", KEYCODE_F3);
+ TERMCAP_TO_KEYCODE.put("k4", KEYCODE_F4);
+ TERMCAP_TO_KEYCODE.put("k5", KEYCODE_F5);
+ TERMCAP_TO_KEYCODE.put("k6", KEYCODE_F6);
+ TERMCAP_TO_KEYCODE.put("k7", KEYCODE_F7);
+ TERMCAP_TO_KEYCODE.put("k8", KEYCODE_F8);
+ TERMCAP_TO_KEYCODE.put("k9", KEYCODE_F9);
+ TERMCAP_TO_KEYCODE.put("k;", KEYCODE_F10);
+ TERMCAP_TO_KEYCODE.put("F1", KEYCODE_F11);
+ TERMCAP_TO_KEYCODE.put("F2", KEYCODE_F12);
+ TERMCAP_TO_KEYCODE.put("F3", KEYMOD_SHIFT | KEYCODE_F1);
+ TERMCAP_TO_KEYCODE.put("F4", KEYMOD_SHIFT | KEYCODE_F2);
+ TERMCAP_TO_KEYCODE.put("F5", KEYMOD_SHIFT | KEYCODE_F3);
+ TERMCAP_TO_KEYCODE.put("F6", KEYMOD_SHIFT | KEYCODE_F4);
+ TERMCAP_TO_KEYCODE.put("F7", KEYMOD_SHIFT | KEYCODE_F5);
+ TERMCAP_TO_KEYCODE.put("F8", KEYMOD_SHIFT | KEYCODE_F6);
+ TERMCAP_TO_KEYCODE.put("F9", KEYMOD_SHIFT | KEYCODE_F7);
+ TERMCAP_TO_KEYCODE.put("FA", KEYMOD_SHIFT | KEYCODE_F8);
+ TERMCAP_TO_KEYCODE.put("FB", KEYMOD_SHIFT | KEYCODE_F9);
+ TERMCAP_TO_KEYCODE.put("FC", KEYMOD_SHIFT | KEYCODE_F10);
+ TERMCAP_TO_KEYCODE.put("FD", KEYMOD_SHIFT | KEYCODE_F11);
+ TERMCAP_TO_KEYCODE.put("FE", KEYMOD_SHIFT | KEYCODE_F12);
+
+ TERMCAP_TO_KEYCODE.put("kb", KEYCODE_DEL); // backspace key
+
+ TERMCAP_TO_KEYCODE.put("kd", KEYCODE_DPAD_DOWN); // terminfo=kcud1, down-arrow key
+ TERMCAP_TO_KEYCODE.put("kh", KeyEvent.KEYCODE_HOME);
+ TERMCAP_TO_KEYCODE.put("kl", KEYCODE_DPAD_LEFT);
+ TERMCAP_TO_KEYCODE.put("kr", KEYCODE_DPAD_RIGHT);
+
+ // K1=Upper left of keypad:
+ // t_K1 keypad home key
+ // t_K3 keypad page-up key
+ // t_K4 keypad end key
+ // t_K5 keypad page-down key
+ TERMCAP_TO_KEYCODE.put("K1", KeyEvent.KEYCODE_HOME);
+ TERMCAP_TO_KEYCODE.put("K3", KeyEvent.KEYCODE_PAGE_UP);
+ TERMCAP_TO_KEYCODE.put("K4", KeyEvent.KEYCODE_MOVE_END);
+ TERMCAP_TO_KEYCODE.put("K5", KeyEvent.KEYCODE_PAGE_DOWN);
+
+ TERMCAP_TO_KEYCODE.put("ku", KEYCODE_DPAD_UP);
+
+ TERMCAP_TO_KEYCODE.put("kB", KEYMOD_SHIFT | KEYCODE_TAB); // termcap=kB, terminfo=kcbt: Back-tab
+ TERMCAP_TO_KEYCODE.put("kD", KEYCODE_FORWARD_DEL); // terminfo=kdch1, delete-character key
+ TERMCAP_TO_KEYCODE.put("kDN", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // non-standard shifted arrow down
+ TERMCAP_TO_KEYCODE.put("kF", KEYMOD_SHIFT | KEYCODE_DPAD_DOWN); // terminfo=kind, scroll-forward key
+ TERMCAP_TO_KEYCODE.put("kI", KEYCODE_INSERT);
+ TERMCAP_TO_KEYCODE.put("kN", KEYCODE_PAGE_UP);
+ TERMCAP_TO_KEYCODE.put("kP", KEYCODE_PAGE_DOWN);
+ TERMCAP_TO_KEYCODE.put("kR", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // terminfo=kri, scroll-backward key
+ TERMCAP_TO_KEYCODE.put("kUP", KEYMOD_SHIFT | KEYCODE_DPAD_UP); // non-standard shifted up
+
+ TERMCAP_TO_KEYCODE.put("@7", KEYCODE_MOVE_END);
+ TERMCAP_TO_KEYCODE.put("@8", KEYCODE_NUMPAD_ENTER);
+ }
+
+ static String getCodeFromTermcap(String termcap, boolean cursorKeysApplication, boolean keypadApplication) {
+ Integer keyCodeAndMod = TERMCAP_TO_KEYCODE.get(termcap);
+ if (keyCodeAndMod == null) return null;
+ int keyCode = keyCodeAndMod;
+ int keyMod = 0;
+ if ((keyCode & KEYMOD_SHIFT) != 0) {
+ keyMod |= KEYMOD_SHIFT;
+ keyCode &= ~KEYMOD_SHIFT;
+ }
+ if ((keyCode & KEYMOD_CTRL) != 0) {
+ keyMod |= KEYMOD_CTRL;
+ keyCode &= ~KEYMOD_CTRL;
+ }
+ if ((keyCode & KEYMOD_ALT) != 0) {
+ keyMod |= KEYMOD_ALT;
+ keyCode &= ~KEYMOD_ALT;
+ }
+ return getCode(keyCode, keyMod, cursorKeysApplication, keypadApplication);
+ }
+
+ public static String getCode(int keyCode, int keyMode, boolean cursorApp, boolean keypadApplication) {
+ switch (keyCode) {
+ case KEYCODE_DPAD_CENTER:
+ return "\015";
+
+ case KEYCODE_DPAD_UP:
+ return (keyMode == 0) ? (cursorApp ? "\033OA" : "\033[A") : transformForModifiers("\033[1", keyMode, 'A');
+ case KEYCODE_DPAD_DOWN:
+ return (keyMode == 0) ? (cursorApp ? "\033OB" : "\033[B") : transformForModifiers("\033[1", keyMode, 'B');
+ case KEYCODE_DPAD_RIGHT:
+ return (keyMode == 0) ? (cursorApp ? "\033OC" : "\033[C") : transformForModifiers("\033[1", keyMode, 'C');
+ case KEYCODE_DPAD_LEFT:
+ return (keyMode == 0) ? (cursorApp ? "\033OD" : "\033[D") : transformForModifiers("\033[1", keyMode, 'D');
+
+ case KeyEvent.KEYCODE_HOME:
+ return (keyMode == 0) ? (cursorApp ? "\033OH" : "\033[H") : transformForModifiers("\033[1", keyMode, 'H');
+ case KEYCODE_MOVE_END:
+ return (keyMode == 0) ? (cursorApp ? "\033OF" : "\033[F") : transformForModifiers("\033[1", keyMode, 'F');
+
+ // An xterm can send function keys F1 to F4 in two modes: vt100 compatible or
+ // not. Because Vim may not know what the xterm is sending, both types of keys
+ // are recognized. The same happens for the and keys.
+ // normal vt100 ~
+ // t_k1 [11~ OP *-xterm*
+ // t_k2 [12~ OQ *-xterm*
+ // t_k3 [13~ OR *-xterm*
+ // t_k4 [14~ OS *-xterm*
+ // t_kh [7~ OH *-xterm*
+ // t_@7 [4~ OF *-xterm*
+ case KEYCODE_F1:
+ return (keyMode == 0) ? "\033OP" : transformForModifiers("\033[1", keyMode, 'P');
+ case KEYCODE_F2:
+ return (keyMode == 0) ? "\033OQ" : transformForModifiers("\033[1", keyMode, 'Q');
+ case KEYCODE_F3:
+ return (keyMode == 0) ? "\033OR" : transformForModifiers("\033[1", keyMode, 'R');
+ case KEYCODE_F4:
+ return (keyMode == 0) ? "\033OS" : transformForModifiers("\033[1", keyMode, 'S');
+ case KEYCODE_F5:
+ return transformForModifiers("\033[15", keyMode, '~');
+ case KEYCODE_F6:
+ return transformForModifiers("\033[17", keyMode, '~');
+ case KEYCODE_F7:
+ return transformForModifiers("\033[18", keyMode, '~');
+ case KEYCODE_F8:
+ return transformForModifiers("\033[19", keyMode, '~');
+ case KEYCODE_F9:
+ return transformForModifiers("\033[20", keyMode, '~');
+ case KEYCODE_F10:
+ return transformForModifiers("\033[21", keyMode, '~');
+ case KEYCODE_F11:
+ return transformForModifiers("\033[23", keyMode, '~');
+ case KEYCODE_F12:
+ return transformForModifiers("\033[24", keyMode, '~');
+
+ case KEYCODE_SYSRQ:
+ return "\033[32~"; // Sys Request / Print
+ // Is this Scroll lock? case Cancel: return "\033[33~";
+ case KEYCODE_BREAK:
+ return "\033[34~"; // Pause/Break
+
+ case KEYCODE_ESCAPE:
+ case KeyEvent.KEYCODE_BACK:
+ return "\033";
+
+ case KEYCODE_INSERT:
+ return transformForModifiers("\033[2", keyMode, '~');
+ case KEYCODE_FORWARD_DEL:
+ return transformForModifiers("\033[3", keyMode, '~');
+
+ case KEYCODE_NUMPAD_DOT:
+ return keypadApplication ? "\033On" : "\033[3~";
+
+ case KEYCODE_PAGE_UP:
+ return "\033[5~";
+ case KEYCODE_PAGE_DOWN:
+ return "\033[6~";
+ case KEYCODE_DEL:
+ // Yes, this needs to U+007F and not U+0008!
+ return "\u007F";
+ case KEYCODE_NUM_LOCK:
+ return "\033OP";
+
+ case KeyEvent.KEYCODE_SPACE:
+ // If ctrl is not down, return null so that it goes through normal input processing (which may e.g. cause a
+ // combining accent to be written):
+ return ((keyMode & KEYMOD_CTRL) == 0) ? null : "\0";
+ case KEYCODE_TAB:
+ // This is back-tab when shifted:
+ return (keyMode & KEYMOD_SHIFT) == 0 ? "\011" : "\033[Z";
+ case KEYCODE_ENTER:
+ return ((keyMode & KEYMOD_ALT) == 0) ? "\r" : "\033\r";
+
+ case KEYCODE_NUMPAD_ENTER:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'M') : "\n";
+ case KEYCODE_NUMPAD_MULTIPLY:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'j') : "*";
+ case KEYCODE_NUMPAD_ADD:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'k') : "+";
+ case KEYCODE_NUMPAD_COMMA:
+ return ",";
+ case KEYCODE_NUMPAD_SUBTRACT:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'm') : "-";
+ case KEYCODE_NUMPAD_DIVIDE:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'o') : "/";
+ case KEYCODE_NUMPAD_0:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'p') : "1";
+ case KEYCODE_NUMPAD_1:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'q') : "1";
+ case KEYCODE_NUMPAD_2:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'r') : "2";
+ case KEYCODE_NUMPAD_3:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 's') : "3";
+ case KEYCODE_NUMPAD_4:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 't') : "4";
+ case KEYCODE_NUMPAD_5:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'u') : "5";
+ case KEYCODE_NUMPAD_6:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'v') : "6";
+ case KEYCODE_NUMPAD_7:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'w') : "7";
+ case KEYCODE_NUMPAD_8:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'x') : "8";
+ case KEYCODE_NUMPAD_9:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'y') : "9";
+ case KEYCODE_NUMPAD_EQUALS:
+ return keypadApplication ? transformForModifiers("\033O", keyMode, 'X') : "=";
+ }
+
+ return null;
+ }
+
+ private static String transformForModifiers(String start, int keymod, char lastChar) {
+ int modifier;
+ switch (keymod) {
+ case KEYMOD_SHIFT:
+ modifier = 2;
+ break;
+ case KEYMOD_ALT:
+ modifier = 3;
+ break;
+ case (KEYMOD_SHIFT | KEYMOD_ALT):
+ modifier = 4;
+ break;
+ case KEYMOD_CTRL:
+ modifier = 5;
+ break;
+ case KEYMOD_SHIFT | KEYMOD_CTRL:
+ modifier = 6;
+ break;
+ case KEYMOD_ALT | KEYMOD_CTRL:
+ modifier = 7;
+ break;
+ case KEYMOD_SHIFT | KEYMOD_ALT | KEYMOD_CTRL:
+ modifier = 8;
+ break;
+ default:
+ return start + lastChar;
+ }
+ return start + (";" + modifier) + lastChar;
+ }
+}
diff --git a/app/src/main/java/com/termux/terminal/TerminalBuffer.java b/app/src/main/java/com/termux/terminal/TerminalBuffer.java
new file mode 100644
index 0000000000..2305f0e36b
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/TerminalBuffer.java
@@ -0,0 +1,435 @@
+package com.termux.terminal;
+
+/**
+ * A circular buffer of {@link TerminalRow}:s which keeps notes about what is visible on a logical screen and the scroll
+ * history.
+ *
+ * See {@link #externalToInternalRow(int)} for how to map from logical screen rows to array indices.
+ */
+public final class TerminalBuffer {
+
+ TerminalRow[] mLines;
+ /** The length of {@link #mLines}. */
+ int mTotalRows;
+ /** The number of rows and columns visible on the screen. */
+ int mScreenRows, mColumns;
+ /** The number of rows kept in history. */
+ private int mActiveTranscriptRows = 0;
+ /** The index in the circular buffer where the visible screen starts. */
+ private int mScreenFirstRow = 0;
+
+ /**
+ * Create a transcript screen.
+ *
+ * @param columns
+ * the width of the screen in characters.
+ * @param totalRows
+ * the height of the entire text area, in rows of text.
+ * @param screenRows
+ * the height of just the screen, not including the transcript that holds lines that have scrolled off
+ * the top of the screen.
+ */
+ public TerminalBuffer(int columns, int totalRows, int screenRows) {
+ mColumns = columns;
+ mTotalRows = totalRows;
+ mScreenRows = screenRows;
+ mLines = new TerminalRow[totalRows];
+
+ blockSet(0, 0, columns, screenRows, ' ', TextStyle.NORMAL);
+ }
+
+ public String getTranscriptText() {
+ return getSelectedText(0, -getActiveTranscriptRows(), mColumns, mScreenRows).trim();
+ }
+
+ public String getSelectedText(int selX1, int selY1, int selX2, int selY2) {
+ final StringBuilder builder = new StringBuilder();
+ final int columns = mColumns;
+
+ if (selY1 < -getActiveTranscriptRows()) selY1 = -getActiveTranscriptRows();
+ if (selY2 >= mScreenRows) selY2 = mScreenRows - 1;
+
+ for (int row = selY1; row <= selY2; row++) {
+ int x1 = (row == selY1) ? selX1 : 0;
+ int x2;
+ if (row == selY2) {
+ x2 = selX2 + 1;
+ if (x2 > columns) x2 = columns;
+ } else {
+ x2 = columns;
+ }
+ TerminalRow lineObject = mLines[externalToInternalRow(row)];
+ int x1Index = lineObject.findStartOfColumn(x1);
+ int x2Index = (x2 < mColumns) ? lineObject.findStartOfColumn(x2) : lineObject.getSpaceUsed();
+ char[] line = lineObject.mText;
+ int lastPrintingCharIndex = -1;
+ int i;
+ boolean rowLineWrap = getLineWrap(row);
+ if (rowLineWrap && x2 == columns) {
+ // If the line was wrapped, we shouldn't lose trailing space:
+ lastPrintingCharIndex = x2Index - 1;
+ } else {
+ for (i = x1Index; i < x2Index; ++i) {
+ char c = line[i];
+ if (c != ' ' && !Character.isLowSurrogate(c)) lastPrintingCharIndex = i;
+ }
+ }
+ if (lastPrintingCharIndex != -1) builder.append(line, x1Index, lastPrintingCharIndex - x1Index + 1);
+ if (!rowLineWrap && row < selY2 && row < mScreenRows - 1) builder.append('\n');
+ }
+ return builder.toString();
+ }
+
+ public int getActiveTranscriptRows() {
+ return mActiveTranscriptRows;
+ }
+
+ public int getActiveRows() {
+ return mActiveTranscriptRows + mScreenRows;
+ }
+
+ /**
+ * Convert a row value from the public external coordinate system to our internal private coordinate system.
+ *
+ *
+ *
External coordinate system: -mActiveTranscriptRows to mScreenRows-1, with the screen being 0..mScreenRows-1.
+ *
Internal coordinate system: the mScreenRows lines starting at mScreenFirstRow comprise the screen, while the
+ * mActiveTranscriptRows lines ending at mScreenFirstRow-1 form the transcript (as a circular buffer).
+ *
+ *
+ * @param externalRow
+ * a row in the external coordinate system.
+ * @return The row corresponding to the input argument in the private coordinate system.
+ */
+ public int externalToInternalRow(int externalRow) {
+ if (externalRow < -mActiveTranscriptRows || externalRow > mScreenRows)
+ throw new IllegalArgumentException("extRow=" + externalRow + ", mScreenRows=" + mScreenRows + ", mActiveTranscriptRows=" + mActiveTranscriptRows);
+ final int internalRow = mScreenFirstRow + externalRow;
+ return (internalRow < 0) ? (mTotalRows + internalRow) : (internalRow % mTotalRows);
+ }
+
+ public void setLineWrap(int row) {
+ mLines[externalToInternalRow(row)].mLineWrap = true;
+ }
+
+ private boolean getLineWrap(int row) {
+ return mLines[externalToInternalRow(row)].mLineWrap;
+ }
+
+ /**
+ * Resize the screen which this transcript backs. Currently, this only works if the number of columns does not
+ * change or the rows expand (that is, it only works when shrinking the number of rows).
+ *
+ * @param newColumns
+ * The number of columns the screen should have.
+ * @param newRows
+ * The number of rows the screen should have.
+ * @param cursor
+ * An int[2] containing the (column, row) cursor location.
+ */
+ public void resize(int newColumns, int newRows, int newTotalRows, int[] cursor, int currentStyle, boolean altScreen) {
+ // newRows > mTotalRows should not normally happen since mTotalRows is TRANSCRIPT_ROWS (10000):
+ if (newColumns == mColumns && newRows <= mTotalRows) {
+ // Fast resize where just the rows changed.
+ int shiftDownOfTopRow = mScreenRows - newRows;
+ if (shiftDownOfTopRow > 0 && shiftDownOfTopRow < mScreenRows) {
+ // Shrinking. Check if we can skip blank rows at bottom below cursor.
+ for (int i = mScreenRows - 1; i > 0; i--) {
+ if (cursor[1] >= i) break;
+ int r = externalToInternalRow(i);
+ if (mLines[r] == null || mLines[r].isBlank()) {
+ if (--shiftDownOfTopRow == 0) break;
+ }
+ }
+ } else if (shiftDownOfTopRow < 0) {
+ // Negative shift down = expanding. Only move screen up if there is transcript to show:
+ int actualShift = Math.max(shiftDownOfTopRow, -mActiveTranscriptRows);
+ if (shiftDownOfTopRow != actualShift) {
+ // The new lines revealed by the resizing are not all from the transcript. Blank the below ones.
+ for (int i = 0; i < actualShift - shiftDownOfTopRow; i++)
+ allocateFullLineIfNecessary((mScreenFirstRow + mScreenRows + i) % mTotalRows).clear(currentStyle);
+ shiftDownOfTopRow = actualShift;
+ }
+ }
+ mScreenFirstRow += shiftDownOfTopRow;
+ mScreenFirstRow = (mScreenFirstRow < 0) ? (mScreenFirstRow + mTotalRows) : (mScreenFirstRow % mTotalRows);
+ mTotalRows = newTotalRows;
+ mActiveTranscriptRows = altScreen ? 0 : Math.max(0, mActiveTranscriptRows + shiftDownOfTopRow);
+ cursor[1] -= shiftDownOfTopRow;
+ mScreenRows = newRows;
+ } else {
+ // Copy away old state and update new:
+ TerminalRow[] oldLines = mLines;
+ mLines = new TerminalRow[newTotalRows];
+ for (int i = 0; i < newTotalRows; i++)
+ mLines[i] = new TerminalRow(newColumns, currentStyle);
+
+ final int oldActiveTranscriptRows = mActiveTranscriptRows;
+ final int oldScreenFirstRow = mScreenFirstRow;
+ final int oldScreenRows = mScreenRows;
+ final int oldTotalRows = mTotalRows;
+ mTotalRows = newTotalRows;
+ mScreenRows = newRows;
+ mActiveTranscriptRows = mScreenFirstRow = 0;
+ mColumns = newColumns;
+
+ int newCursorRow = -1;
+ int newCursorColumn = -1;
+ int oldCursorRow = cursor[1];
+ int oldCursorColumn = cursor[0];
+ boolean newCursorPlaced = false;
+
+ int currentOutputExternalRow = 0;
+ int currentOutputExternalColumn = 0;
+
+ // Loop over every character in the initial state.
+ // Blank lines should be skipped only if at end of transcript (just as is done in the "fast" resize), so we
+ // keep track how many blank lines we have skipped if we later on find a non-blank line.
+ int skippedBlankLines = 0;
+ for (int externalOldRow = -oldActiveTranscriptRows; externalOldRow < oldScreenRows; externalOldRow++) {
+ // Do what externalToInternalRow() does but for the old state:
+ int internalOldRow = oldScreenFirstRow + externalOldRow;
+ internalOldRow = (internalOldRow < 0) ? (oldTotalRows + internalOldRow) : (internalOldRow % oldTotalRows);
+
+ TerminalRow oldLine = oldLines[internalOldRow];
+ boolean cursorAtThisRow = externalOldRow == oldCursorRow;
+ // The cursor may only be on a non-null line, which we should not skip:
+ if (oldLine == null || (!(!newCursorPlaced && cursorAtThisRow)) && oldLine.isBlank()) {
+ skippedBlankLines++;
+ continue;
+ } else if (skippedBlankLines > 0) {
+ // After skipping some blank lines we encounter a non-blank line. Insert the skipped blank lines.
+ for (int i = 0; i < skippedBlankLines; i++) {
+ if (currentOutputExternalRow == mScreenRows - 1) {
+ scrollDownOneLine(0, mScreenRows, currentStyle);
+ } else {
+ currentOutputExternalRow++;
+ }
+ currentOutputExternalColumn = 0;
+ }
+ skippedBlankLines = 0;
+ }
+
+ int lastNonSpaceIndex = 0;
+ boolean justToCursor = false;
+ if (cursorAtThisRow || oldLine.mLineWrap) {
+ // Take the whole line, either because of cursor on it, or if line wrapping.
+ lastNonSpaceIndex = oldLine.getSpaceUsed();
+ if (cursorAtThisRow) justToCursor = true;
+ } else {
+ for (int i = 0; i < oldLine.getSpaceUsed(); i++)
+ // NEWLY INTRODUCED BUG! Should not index oldLine.mStyle with char indices
+ if (oldLine.mText[i] != ' '/* || oldLine.mStyle[i] != currentStyle */) lastNonSpaceIndex = i + 1;
+ }
+
+ int currentOldCol = 0;
+ int styleAtCol = 0;
+ for (int i = 0; i < lastNonSpaceIndex; i++) {
+ // Note that looping over java character, not cells.
+ char c = oldLine.mText[i];
+ int codePoint = (Character.isHighSurrogate(c)) ? Character.toCodePoint(c, oldLine.mText[++i]) : c;
+ int displayWidth = WcWidth.width(codePoint);
+ // Use the last style if this is a zero-width character:
+ if (displayWidth > 0) styleAtCol = oldLine.getStyle(currentOldCol);
+
+ // Line wrap as necessary:
+ if (currentOutputExternalColumn + displayWidth > mColumns) {
+ setLineWrap(currentOutputExternalRow);
+ if (currentOutputExternalRow == mScreenRows - 1) {
+ if (newCursorPlaced) newCursorRow--;
+ scrollDownOneLine(0, mScreenRows, currentStyle);
+ } else {
+ currentOutputExternalRow++;
+ }
+ currentOutputExternalColumn = 0;
+ }
+
+ int offsetDueToCombiningChar = ((displayWidth <= 0 && currentOutputExternalColumn > 0) ? 1 : 0);
+ int outputColumn = currentOutputExternalColumn - offsetDueToCombiningChar;
+ setChar(outputColumn, currentOutputExternalRow, codePoint, styleAtCol);
+
+ if (displayWidth > 0) {
+ if (oldCursorRow == externalOldRow && oldCursorColumn == currentOldCol) {
+ newCursorColumn = currentOutputExternalColumn;
+ newCursorRow = currentOutputExternalRow;
+ newCursorPlaced = true;
+ }
+ currentOldCol += displayWidth;
+ currentOutputExternalColumn += displayWidth;
+ if (justToCursor && newCursorPlaced) break;
+ }
+ }
+ // Old row has been copied. Check if we need to insert newline if old line was not wrapping:
+ if (externalOldRow != (oldScreenRows - 1) && !oldLine.mLineWrap) {
+ if (currentOutputExternalRow == mScreenRows - 1) {
+ if (newCursorPlaced) newCursorRow--;
+ scrollDownOneLine(0, mScreenRows, currentStyle);
+ } else {
+ currentOutputExternalRow++;
+ }
+ currentOutputExternalColumn = 0;
+ }
+ }
+
+ cursor[0] = newCursorColumn;
+ cursor[1] = newCursorRow;
+ }
+
+ // Handle cursor scrolling off screen:
+ if (cursor[0] < 0 || cursor[1] < 0) cursor[0] = cursor[1] = 0;
+ }
+
+ /**
+ * Block copy lines and associated metadata from one location to another in the circular buffer, taking wraparound
+ * into account.
+ *
+ * @param srcInternal
+ * The first line to be copied.
+ * @param len
+ * The number of lines to be copied.
+ */
+ private void blockCopyLinesDown(int srcInternal, int len) {
+ if (len == 0) return;
+ int totalRows = mTotalRows;
+
+ int start = len - 1;
+ // Save away line to be overwritten:
+ TerminalRow lineToBeOverWritten = mLines[(srcInternal + start + 1) % totalRows];
+ // Do the copy from bottom to top.
+ for (int i = start; i >= 0; --i)
+ mLines[(srcInternal + i + 1) % totalRows] = mLines[(srcInternal + i) % totalRows];
+ // Put back overwritten line, now above the block:
+ mLines[(srcInternal) % totalRows] = lineToBeOverWritten;
+ }
+
+ /**
+ * Scroll the screen down one line. To scroll the whole screen of a 24 line screen, the arguments would be (0, 24).
+ *
+ * @param topMargin
+ * First line that is scrolled.
+ * @param bottomMargin
+ * One line after the last line that is scrolled.
+ * @param style
+ * the style for the newly exposed line.
+ */
+ public void scrollDownOneLine(int topMargin, int bottomMargin, int style) {
+ if (topMargin > bottomMargin - 1 || topMargin < 0 || bottomMargin > mScreenRows)
+ throw new IllegalArgumentException("topMargin=" + topMargin + ", bottomMargin=" + bottomMargin + ", mScreenRows=" + mScreenRows);
+
+ // Copy the fixed topMargin lines one line down so that they remain on screen in same position:
+ blockCopyLinesDown(mScreenFirstRow, topMargin);
+ // Copy the fixed mScreenRows-bottomMargin lines one line down so that they remain on screen in same
+ // position:
+ blockCopyLinesDown(externalToInternalRow(bottomMargin), mScreenRows - bottomMargin);
+
+ // Update the screen location in the ring buffer:
+ mScreenFirstRow = (mScreenFirstRow + 1) % mTotalRows;
+ // Note that the history has grown if not already full:
+ if (mActiveTranscriptRows < mTotalRows - mScreenRows) mActiveTranscriptRows++;
+
+ // Blank the newly revealed line above the bottom margin:
+ int blankRow = externalToInternalRow(bottomMargin - 1);
+ if (mLines[blankRow] == null) {
+ mLines[blankRow] = new TerminalRow(mColumns, style);
+ } else {
+ mLines[blankRow].clear(style);
+ }
+ }
+
+ /**
+ * Block copy characters from one position in the screen to another. The two positions can overlap. All characters
+ * of the source and destination must be within the bounds of the screen, or else an InvalidParameterException will
+ * be thrown.
+ *
+ * @param sx
+ * source X coordinate
+ * @param sy
+ * source Y coordinate
+ * @param w
+ * width
+ * @param h
+ * height
+ * @param dx
+ * destination X coordinate
+ * @param dy
+ * destination Y coordinate
+ */
+ public void blockCopy(int sx, int sy, int w, int h, int dx, int dy) {
+ if (w == 0) return;
+ if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows || dx < 0 || dx + w > mColumns || dy < 0 || dy + h > mScreenRows)
+ throw new IllegalArgumentException();
+ boolean copyingUp = sy > dy;
+ for (int y = 0; y < h; y++) {
+ int y2 = copyingUp ? y : (h - (y + 1));
+ TerminalRow sourceRow = allocateFullLineIfNecessary(externalToInternalRow(sy + y2));
+ allocateFullLineIfNecessary(externalToInternalRow(dy + y2)).copyInterval(sourceRow, sx, sx + w, dx);
+ }
+ }
+
+ /**
+ * Block set characters. All characters must be within the bounds of the screen, or else and
+ * InvalidParemeterException will be thrown. Typically this is called with a "val" argument of 32 to clear a block
+ * of characters.
+ */
+ public void blockSet(int sx, int sy, int w, int h, int val, int style) {
+ if (sx < 0 || sx + w > mColumns || sy < 0 || sy + h > mScreenRows) {
+ throw new IllegalArgumentException(
+ "Illegal arguments! blockSet(" + sx + ", " + sy + ", " + w + ", " + h + ", " + val + ", " + mColumns + ", " + mScreenRows + ")");
+ }
+ for (int y = 0; y < h; y++)
+ for (int x = 0; x < w; x++)
+ setChar(sx + x, sy + y, val, style);
+ }
+
+ public TerminalRow allocateFullLineIfNecessary(int row) {
+ return (mLines[row] == null) ? (mLines[row] = new TerminalRow(mColumns, 0)) : mLines[row];
+ }
+
+ public void setChar(int column, int row, int codePoint, int style) {
+ if (row >= mScreenRows || column >= mColumns)
+ throw new IllegalArgumentException("row=" + row + ", column=" + column + ", mScreenRows=" + mScreenRows + ", mColumns=" + mColumns);
+ row = externalToInternalRow(row);
+ allocateFullLineIfNecessary(row).setChar(column, codePoint, style);
+ }
+
+ public int getStyleAt(int externalRow, int column) {
+ return allocateFullLineIfNecessary(externalToInternalRow(externalRow)).getStyle(column);
+ }
+
+ /** Support for http://vt100.net/docs/vt510-rm/DECCARA and http://vt100.net/docs/vt510-rm/DECCARA */
+ public void setOrClearEffect(int bits, boolean setOrClear, boolean reverse, boolean rectangular, int leftMargin, int rightMargin, int top, int left,
+ int bottom, int right) {
+ for (int y = top; y < bottom; y++) {
+ TerminalRow line = mLines[externalToInternalRow(y)];
+ int startOfLine = (rectangular || y == top) ? left : leftMargin;
+ int endOfLine = (rectangular || y + 1 == bottom) ? right : rightMargin;
+ for (int x = startOfLine; x < endOfLine; x++) {
+ int currentStyle = line.getStyle(x);
+ int foreColor = TextStyle.decodeForeColor(currentStyle);
+ int backColor = TextStyle.decodeBackColor(currentStyle);
+ int effect = TextStyle.decodeEffect(currentStyle);
+ if (reverse) {
+ // Clear out the bits to reverse and add them back in reversed:
+ effect = (effect & ~bits) | (bits & ~effect);
+ } else if (setOrClear) {
+ effect |= bits;
+ } else {
+ effect &= ~bits;
+ }
+ line.mStyle[x] = TextStyle.encode(foreColor, backColor, effect);
+ }
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/termux/terminal/TerminalColorScheme.java b/app/src/main/java/com/termux/terminal/TerminalColorScheme.java
new file mode 100644
index 0000000000..1721dc1978
--- /dev/null
+++ b/app/src/main/java/com/termux/terminal/TerminalColorScheme.java
@@ -0,0 +1,102 @@
+package com.termux.terminal;
+
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Color scheme for a terminal with default colors, which may be overridden (and then reset) from the shell using
+ * Operating System Control (OSC) sequences.
+ *
+ * @see TerminalColors
+ */
+public final class TerminalColorScheme {
+
+ /** http://upload.wikimedia.org/wikipedia/en/1/15/Xterm_256color_chart.svg, but with blue color brighter. */
+ private static final int[] DEFAULT_COLORSCHEME = {
+ // 16 original colors. First 8 are dim.
+ 0xff000000, // black
+ 0xffcd0000, // dim red
+ 0xff00cd00, // dim green
+ 0xffcdcd00, // dim yellow
+ 0xff6495ed, // dim blue
+ 0xffcd00cd, // dim magenta
+ 0xff00cdcd, // dim cyan
+ 0xffe5e5e5, // dim white
+ // Second 8 are bright:
+ 0xff7f7f7f, // medium grey
+ 0xffff0000, // bright red
+ 0xff00ff00, // bright green
+ 0xffffff00, // bright yellow
+ 0xff5c5cff, // light blue
+ 0xffff00ff, // bright magenta
+ 0xff00ffff, // bright cyan
+ 0xffffffff, // bright white
+
+ // 216 color cube, six shades of each color:
+ 0xff000000, 0xff00005f, 0xff000087, 0xff0000af, 0xff0000d7, 0xff0000ff, 0xff005f00, 0xff005f5f, 0xff005f87, 0xff005faf, 0xff005fd7, 0xff005fff,
+ 0xff008700, 0xff00875f, 0xff008787, 0xff0087af, 0xff0087d7, 0xff0087ff, 0xff00af00, 0xff00af5f, 0xff00af87, 0xff00afaf, 0xff00afd7, 0xff00afff,
+ 0xff00d700, 0xff00d75f, 0xff00d787, 0xff00d7af, 0xff00d7d7, 0xff00d7ff, 0xff00ff00, 0xff00ff5f, 0xff00ff87, 0xff00ffaf, 0xff00ffd7, 0xff00ffff,
+ 0xff5f0000, 0xff5f005f, 0xff5f0087, 0xff5f00af, 0xff5f00d7, 0xff5f00ff, 0xff5f5f00, 0xff5f5f5f, 0xff5f5f87, 0xff5f5faf, 0xff5f5fd7, 0xff5f5fff,
+ 0xff5f8700, 0xff5f875f, 0xff5f8787, 0xff5f87af, 0xff5f87d7, 0xff5f87ff, 0xff5faf00, 0xff5faf5f, 0xff5faf87, 0xff5fafaf, 0xff5fafd7, 0xff5fafff,
+ 0xff5fd700, 0xff5fd75f, 0xff5fd787, 0xff5fd7af, 0xff5fd7d7, 0xff5fd7ff, 0xff5fff00, 0xff5fff5f, 0xff5fff87, 0xff5fffaf, 0xff5fffd7, 0xff5fffff,
+ 0xff870000, 0xff87005f, 0xff870087, 0xff8700af, 0xff8700d7, 0xff8700ff, 0xff875f00, 0xff875f5f, 0xff875f87, 0xff875faf, 0xff875fd7, 0xff875fff,
+ 0xff878700, 0xff87875f, 0xff878787, 0xff8787af, 0xff8787d7, 0xff8787ff, 0xff87af00, 0xff87af5f, 0xff87af87, 0xff87afaf, 0xff87afd7, 0xff87afff,
+ 0xff87d700, 0xff87d75f, 0xff87d787, 0xff87d7af, 0xff87d7d7, 0xff87d7ff, 0xff87ff00, 0xff87ff5f, 0xff87ff87, 0xff87ffaf, 0xff87ffd7, 0xff87ffff,
+ 0xffaf0000, 0xffaf005f, 0xffaf0087, 0xffaf00af, 0xffaf00d7, 0xffaf00ff, 0xffaf5f00, 0xffaf5f5f, 0xffaf5f87, 0xffaf5faf, 0xffaf5fd7, 0xffaf5fff,
+ 0xffaf8700, 0xffaf875f, 0xffaf8787, 0xffaf87af, 0xffaf87d7, 0xffaf87ff, 0xffafaf00, 0xffafaf5f, 0xffafaf87, 0xffafafaf, 0xffafafd7, 0xffafafff,
+ 0xffafd700, 0xffafd75f, 0xffafd787, 0xffafd7af, 0xffafd7d7, 0xffafd7ff, 0xffafff00, 0xffafff5f, 0xffafff87, 0xffafffaf, 0xffafffd7, 0xffafffff,
+ 0xffd70000, 0xffd7005f, 0xffd70087, 0xffd700af, 0xffd700d7, 0xffd700ff, 0xffd75f00, 0xffd75f5f, 0xffd75f87, 0xffd75faf, 0xffd75fd7, 0xffd75fff,
+ 0xffd78700, 0xffd7875f, 0xffd78787, 0xffd787af, 0xffd787d7, 0xffd787ff, 0xffd7af00, 0xffd7af5f, 0xffd7af87, 0xffd7afaf, 0xffd7afd7, 0xffd7afff,
+ 0xffd7d700, 0xffd7d75f, 0xffd7d787, 0xffd7d7af, 0xffd7d7d7, 0xffd7d7ff, 0xffd7ff00, 0xffd7ff5f, 0xffd7ff87, 0xffd7ffaf, 0xffd7ffd7, 0xffd7ffff,
+ 0xffff0000, 0xffff005f, 0xffff0087, 0xffff00af, 0xffff00d7, 0xffff00ff, 0xffff5f00, 0xffff5f5f, 0xffff5f87, 0xffff5faf, 0xffff5fd7, 0xffff5fff,
+ 0xffff8700, 0xffff875f, 0xffff8787, 0xffff87af, 0xffff87d7, 0xffff87ff, 0xffffaf00, 0xffffaf5f, 0xffffaf87, 0xffffafaf, 0xffffafd7, 0xffffafff,
+ 0xffffd700, 0xffffd75f, 0xffffd787, 0xffffd7af, 0xffffd7d7, 0xffffd7ff, 0xffffff00, 0xffffff5f, 0xffffff87, 0xffffffaf, 0xffffffd7, 0xffffffff,
+
+ // 24 grey scale ramp:
+ 0xff080808, 0xff121212, 0xff1c1c1c, 0xff262626, 0xff303030, 0xff3a3a3a, 0xff444444, 0xff4e4e4e, 0xff585858, 0xff626262, 0xff6c6c6c, 0xff767676,
+ 0xff808080, 0xff8a8a8a, 0xff949494, 0xff9e9e9e, 0xffa8a8a8, 0xffb2b2b2, 0xffbcbcbc, 0xffc6c6c6, 0xffd0d0d0, 0xffdadada, 0xffe4e4e4, 0xffeeeeee,
+
+ // COLOR_INDEX_DEFAULT_FOREGROUND, COLOR_INDEX_DEFAULT_BACKGROUND and COLOR_INDEX_DEFAULT_CURSOR:
+ 0xffffffff, 0xff000000, 0xffffffff };
+
+ public final int[] mDefaultColors = new int[TextStyle.NUM_INDEXED_COLORS];
+
+ public TerminalColorScheme() {
+ reset();
+ }
+
+ public void reset() {
+ System.arraycopy(DEFAULT_COLORSCHEME, 0, mDefaultColors, 0, TextStyle.NUM_INDEXED_COLORS);
+ }
+
+ public void updateWith(Properties props) {
+ reset();
+ for (Map.Entry