├── .gitignore ├── .gitmodules ├── .idea ├── codeStyles │ └── Project.xml ├── dictionaries │ └── mao.xml ├── encodings.xml └── runConfigurations.xml ├── LICENSE ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── github │ │ └── maoabc │ │ └── aterm │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── maoabc │ │ │ ├── BaseApp.java │ │ │ ├── aterm │ │ │ ├── ATermActivity.java │ │ │ ├── ATermIntentHandler.java │ │ │ ├── ATermPreferenceFragment.java │ │ │ ├── ATermService.java │ │ │ ├── ATermSettings.java │ │ │ ├── ATermSettingsActivity.java │ │ │ ├── AndroidTerminal.java │ │ │ ├── BellUtil.java │ │ │ ├── CreateSshServerDialogFragment.java │ │ │ ├── TerminalListAdapter.java │ │ │ ├── TerminalManagerDialogFragment.java │ │ │ ├── db │ │ │ │ ├── ATermDatabase.java │ │ │ │ ├── entities │ │ │ │ │ └── SshServer.java │ │ │ │ └── source │ │ │ │ │ ├── SshServerDao.java │ │ │ │ │ └── SshServerDataSource.java │ │ │ ├── ssh │ │ │ │ ├── ByteQueue.java │ │ │ │ └── SshTerminal.java │ │ │ └── viewmodel │ │ │ │ ├── SshServerOption.java │ │ │ │ ├── TerminalItem.java │ │ │ │ └── TerminalManagerViewModel.java │ │ │ ├── common │ │ │ ├── fragment │ │ │ │ ├── RetainedDialogFragment.java │ │ │ │ └── TextFieldDialogFragment.java │ │ │ └── widget │ │ │ │ ├── CheckableButton.java │ │ │ │ ├── CheckableDividerRelativeLayout.java │ │ │ │ ├── DividerRelativeLayout.java │ │ │ │ ├── FixedLinearLayoutManager.java │ │ │ │ └── LongPressRepeatImageView.java │ │ │ └── util │ │ │ ├── AppExecutors.java │ │ │ ├── FileUtils.java │ │ │ ├── MimeTypes.java │ │ │ ├── Precondition.java │ │ │ └── RecyclerViewAdapterChangedCallback.java │ └── res │ │ ├── color │ │ └── checked_button_color.xml │ │ ├── drawable-hdpi │ │ └── ic_stat_terminal.png │ │ ├── drawable-mdpi │ │ └── ic_stat_terminal.png │ │ ├── drawable-xhdpi │ │ └── ic_stat_terminal.png │ │ ├── drawable-xxhdpi │ │ └── ic_stat_terminal.png │ │ ├── drawable-xxxhdpi │ │ └── ic_stat_terminal.png │ │ ├── drawable │ │ ├── ic_add_24dp.xml │ │ ├── ic_arrow_back_24dp.xml │ │ ├── ic_arrow_downward_24dp.xml │ │ ├── ic_arrow_forward_24dp.xml │ │ ├── ic_arrow_upward_24dp.xml │ │ ├── ic_cancel_24dp.xml │ │ ├── ic_launcher_foreground.xml │ │ └── side_nav_bar.xml │ │ ├── layout │ │ ├── activity_aterm.xml │ │ ├── activity_aterm_settings.xml │ │ ├── dialog_create_ssh_server.xml │ │ ├── fragment_text_field.xml │ │ ├── list_item.xml │ │ ├── recycler_view.xml │ │ └── terminal_list_item.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── values │ │ ├── arrays.xml │ │ ├── arraysNoLocalize.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ │ └── xml │ │ ├── aterm_preferences.xml │ │ └── backup_descriptor.xml │ └── test │ └── java │ └── com │ └── github │ └── maoabc │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | /.idea/ 11 | .DS_Store 12 | /build 13 | /captures 14 | .externalNativeBuild 15 | .cxx 16 | /app/release 17 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "terminalview"] 2 | path = terminalview 3 | url = git@github.com:maoabc/aterminalview.git 4 | [submodule "pty"] 5 | path = pty 6 | url = git@github.com:maoabc/aterminalpty.git 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | xmlns:android 14 | 15 | ^$ 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | xmlns:.* 25 | 26 | ^$ 27 | 28 | 29 | BY_NAME 30 | 31 |
32 |
33 | 34 | 35 | 36 | .*:id 37 | 38 | http://schemas.android.com/apk/res/android 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | .*:name 48 | 49 | http://schemas.android.com/apk/res/android 50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | 58 | name 59 | 60 | ^$ 61 | 62 | 63 | 64 |
65 |
66 | 67 | 68 | 69 | style 70 | 71 | ^$ 72 | 73 | 74 | 75 |
76 |
77 | 78 | 79 | 80 | .* 81 | 82 | ^$ 83 | 84 | 85 | BY_NAME 86 | 87 |
88 |
89 | 90 | 91 | 92 | .* 93 | 94 | http://schemas.android.com/apk/res/android 95 | 96 | 97 | ANDROID_ATTRIBUTE_ORDER 98 | 99 |
100 |
101 | 102 | 103 | 104 | .* 105 | 106 | .* 107 | 108 | 109 | BY_NAME 110 | 111 |
112 |
113 |
114 |
115 |
116 |
-------------------------------------------------------------------------------- /.idea/dictionaries/mao.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 29 5 | 6 | 7 | defaultConfig { 8 | applicationId "com.github.maoabc.aterm" 9 | minSdkVersion 21 10 | targetSdkVersion 29 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | 16 | javaCompileOptions { 17 | annotationProcessorOptions { 18 | arguments = [eventBusIndex: 'my.MyEventBusIndex'] 19 | } 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | 30 | dataBinding { 31 | enabled = true 32 | } 33 | compileOptions { 34 | sourceCompatibility = 1.8 35 | targetCompatibility = 1.8 36 | } 37 | lintOptions { 38 | abortOnError false 39 | } 40 | 41 | 42 | } 43 | 44 | dependencies { 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | 47 | implementation 'androidx.appcompat:appcompat:1.1.0' 48 | implementation 'com.google.android.material:material:1.1.0', { 49 | exclude group: 'androidx.viewpager2', module: 'viewpager2' 50 | } 51 | implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.0.0' 52 | 53 | 54 | annotationProcessor 'androidx.room:room-compiler:2.2.5' 55 | implementation 'androidx.room:room-runtime:2.2.5' 56 | 57 | implementation "androidx.recyclerview:recyclerview:1.1.0" 58 | implementation "androidx.preference:preference:1.1.1" 59 | 60 | implementation 'org.greenrobot:eventbus:3.1.1' 61 | annotationProcessor 'org.greenrobot:eventbus-annotation-processor:3.1.1' 62 | 63 | 64 | implementation project(':pty') 65 | implementation project(':terminalview') 66 | // https://mvnrepository.com/artifact/com.jcraft/jsch 67 | implementation 'com.jcraft:jsch:0.1.55' 68 | // https://mvnrepository.com/artifact/com.jcraft/jzlib 69 | implementation 'com.jcraft:jzlib:1.1.3' 70 | 71 | 72 | testImplementation 'junit:junit:4.13' 73 | androidTestImplementation 'androidx.test.ext:junit:1.1.1' 74 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | #eventbus 24 | -keepclassmembers class * { 25 | @org.greenrobot.eventbus.Subscribe ; 26 | } 27 | -keep enum org.greenrobot.eventbus.ThreadMode { *; } 28 | 29 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/github/maoabc/aterm/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.ext.junit.runners.AndroidJUnit4; 6 | import androidx.test.platform.app.InstrumentationRegistry; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("com.github.maoabc.aterm", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 19 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/BaseApp.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc; 2 | 3 | import android.app.Application; 4 | import android.view.Gravity; 5 | import android.widget.Toast; 6 | 7 | import androidx.annotation.StringRes; 8 | 9 | import com.github.maoabc.util.AppExecutors; 10 | 11 | public class BaseApp extends Application { 12 | private static BaseApp baseApp; 13 | private AppExecutors mAppExecutors; 14 | 15 | @Override 16 | public void onCreate() { 17 | super.onCreate(); 18 | mAppExecutors=new AppExecutors(); 19 | baseApp = this; 20 | } 21 | 22 | public static BaseApp get() { 23 | return baseApp; 24 | } 25 | 26 | public AppExecutors getAppExecutors() { 27 | return mAppExecutors; 28 | } 29 | 30 | public static void toast(@StringRes int strId) { 31 | Toast.makeText(get(), strId, Toast.LENGTH_LONG).show(); 32 | } 33 | 34 | public static void toast(String str) { 35 | Toast.makeText(get(), str, Toast.LENGTH_LONG).show(); 36 | } 37 | 38 | 39 | public static void toastTop(final String str) { 40 | Toast toast = Toast.makeText(get(), str, Toast.LENGTH_SHORT); 41 | toast.setGravity(Gravity.TOP, 0, 0); 42 | toast.show(); 43 | } 44 | 45 | public static String getResString(@StringRes int strId, Object... args) { 46 | return BaseApp.get().getString(strId, args); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.content.ComponentName; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.ServiceConnection; 7 | import android.content.SharedPreferences; 8 | import android.content.res.Configuration; 9 | import android.graphics.Typeface; 10 | import android.os.Bundle; 11 | import android.os.Handler; 12 | import android.os.IBinder; 13 | import android.text.TextUtils; 14 | import android.util.DisplayMetrics; 15 | import android.view.KeyEvent; 16 | import android.view.View; 17 | import android.view.inputmethod.InputMethodManager; 18 | import android.widget.Checkable; 19 | 20 | import androidx.annotation.NonNull; 21 | import androidx.appcompat.app.AppCompatActivity; 22 | import androidx.core.view.GravityCompat; 23 | import androidx.drawerlayout.widget.DrawerLayout; 24 | import androidx.recyclerview.widget.RecyclerView; 25 | 26 | import com.github.maoabc.BaseApp; 27 | import com.github.maoabc.aterm.db.entities.SshServer; 28 | import com.github.maoabc.aterm.ssh.SshTerminal; 29 | import com.github.maoabc.aterm.viewmodel.TerminalItem; 30 | import com.github.maoabc.common.fragment.TextFieldDialogFragment; 31 | import com.github.maoabc.common.widget.CheckableButton; 32 | 33 | import org.greenrobot.eventbus.EventBus; 34 | import org.greenrobot.eventbus.Subscribe; 35 | 36 | import aterm.terminal.AbstractTerminal; 37 | import aterm.terminal.TerminalKeys; 38 | import aterm.terminal.TerminalView; 39 | import aterm.terminal.UpdateCallback; 40 | 41 | 42 | public class ATermActivity extends AppCompatActivity { 43 | public static final String TAG = ATermActivity.class.getName(); 44 | public static final boolean DEBUG = BuildConfig.DEBUG; 45 | 46 | private final DisplayMetrics metrics = new DisplayMetrics(); 47 | 48 | private ATermService mATermService; 49 | 50 | private ServiceConnection mTSConnection = new ServiceConnection() { 51 | public void onServiceConnected(ComponentName className, IBinder service) { 52 | mATermService = ((ATermService.TSBinder) service).getService(); 53 | 54 | init(); 55 | } 56 | 57 | public void onServiceDisconnected(ComponentName arg0) { 58 | if (mATermService != null) { 59 | mATermService.currentTerminal.removeObservers(ATermActivity.this); 60 | } 61 | mATermService = null; 62 | } 63 | }; 64 | SharedPreferences.OnSharedPreferenceChangeListener mTermPreferChanged = new SharedPreferences.OnSharedPreferenceChangeListener() { 65 | @Override 66 | public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { 67 | if (mATermService != null) { 68 | mATermService.mSettings.readPrefs(sharedPreferences); 69 | updateTerminalPrefs(mATermService.mSettings); 70 | } 71 | } 72 | }; 73 | private TerminalView mTerminalView; 74 | private boolean mHaveFullHwKeyboard; 75 | private CheckableButton mCtrlChecked; 76 | 77 | private Handler handler = new Handler(); 78 | private DrawerLayout mDrawerLayout; 79 | private BellUtil bellUtil; 80 | 81 | @Override 82 | protected void onCreate(Bundle savedInstanceState) { 83 | super.onCreate(savedInstanceState); 84 | 85 | setContentView(R.layout.activity_aterm); 86 | // Window window = getWindow(); 87 | // window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 88 | EventBus.getDefault().register(this); 89 | 90 | mDrawerLayout = findViewById(R.id.drawer_layout); 91 | 92 | 93 | mTerminalView = findViewById(R.id.emulator_view); 94 | 95 | findViewById(R.id.btn_esc).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_ESCAPE)); 96 | mCtrlChecked = findViewById(R.id.btn_ctrl); 97 | mCtrlChecked.setOnClickListener(v -> { 98 | if (v instanceof Checkable) { 99 | ((Checkable) v).toggle(); 100 | 101 | boolean checked = ((Checkable) v).isChecked(); 102 | int modifiers = mTerminalView.getModifiers(); 103 | mTerminalView.setModifiers(checked ? 104 | modifiers | TerminalKeys.VTERM_MOD_CTRL : 105 | modifiers & ~TerminalKeys.VTERM_MOD_CTRL); 106 | 107 | } 108 | }); 109 | 110 | findViewById(R.id.btn_tab).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_TAB)); 111 | findViewById(R.id.btn_minus).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_MINUS)); 112 | findViewById(R.id.btn_colon).setOnClickListener(v -> { 113 | if (mTerminalView != null) { 114 | AbstractTerminal terminal = mTerminalView.getTerminal(); 115 | if (terminal != null) { 116 | byte[] bytes = ":".getBytes(); 117 | terminal.writeToPty(bytes, bytes.length); 118 | terminal.flushToPty(); 119 | } 120 | } 121 | }); 122 | findViewById(R.id.btn_left).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_DPAD_LEFT)); 123 | findViewById(R.id.btn_up).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_DPAD_UP)); 124 | findViewById(R.id.btn_down).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_DPAD_DOWN)); 125 | findViewById(R.id.btn_right).setOnClickListener(v -> sendKeyCode(KeyEvent.KEYCODE_DPAD_RIGHT)); 126 | 127 | View menuView = findViewById(R.id.btn__nav_overflow); 128 | menuView.setOnClickListener(v -> { 129 | Intent intent = new Intent(ATermActivity.this, ATermSettingsActivity.class); 130 | startActivity(intent); 131 | }); 132 | 133 | 134 | Intent intent = new Intent(this, ATermService.class); 135 | startService(intent); 136 | if (!bindService(intent, mTSConnection, 0)) { 137 | throw new IllegalStateException("Failed to bind to TermService!"); 138 | } 139 | findViewById(R.id.btn_nav_add_term).setOnClickListener(v -> { 140 | TerminalManagerDialogFragment fragment = TerminalManagerDialogFragment.newInstance(); 141 | fragment.show(getSupportFragmentManager(), null); 142 | }); 143 | 144 | bellUtil = BellUtil.getInstance(getApplicationContext()); 145 | 146 | } 147 | 148 | @Subscribe 149 | public void onItemClick(TerminalItem.ItemClickEvent event) { 150 | if (mATermService == null) { 151 | return; 152 | } 153 | SshServer sshServer = event.item.getSshServer(); 154 | if (sshServer == null) { 155 | AbstractTerminal localTerminal = mATermService.createLocalTerminal(); 156 | localTerminal.start(); 157 | mATermService.addTerminal(localTerminal); 158 | } else {//添加ssh终端 159 | SshTerminal sshTerminal = mATermService.createSshTerminal(sshServer.getHost(), sshServer.getPort(), 160 | sshServer.getUsername(), sshServer.getPassword(), 161 | sshServer.getPrivateKey(), sshServer.getPrivateKeyPhase()); 162 | sshTerminal.start(); 163 | mATermService.addTerminal(sshTerminal); 164 | 165 | } 166 | } 167 | 168 | @Subscribe 169 | public void onEditTerminalTitle(ATermService.TerminalLongClickEvent event) { 170 | AbstractTerminal terminal = event.terminal; 171 | TextFieldDialogFragment fragment = TextFieldDialogFragment.newInstance( 172 | getString(R.string.edit_session_name), 173 | "", terminal.getTitle()); 174 | fragment.setResultCallback(text -> { 175 | if (TextUtils.isEmpty(text)) { 176 | return; 177 | } 178 | terminal.setTitle(text); 179 | if (mATermService != null) {//change item 180 | int index = mATermService.terminals.indexOf(terminal); 181 | if (index != -1) { 182 | mATermService.terminals.set(index, terminal); 183 | } 184 | } 185 | 186 | }); 187 | fragment.show(getSupportFragmentManager(), null); 188 | 189 | } 190 | 191 | @Subscribe 192 | public void onFinishActivityEvent(ATermService.FinishTerminalActivityEvent event) { 193 | finish(); 194 | } 195 | 196 | @Override 197 | public void onBackPressed() { 198 | if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { 199 | mDrawerLayout.closeDrawer(GravityCompat.START); 200 | } else { 201 | super.onBackPressed(); 202 | } 203 | } 204 | 205 | @Override 206 | protected void onDestroy() { 207 | super.onDestroy(); 208 | 209 | if (mTSConnection != null) unbindService(mTSConnection); 210 | 211 | if (mTerminalView != null) { 212 | mTerminalView.detachCurrentTerminal(); 213 | } 214 | if (mATermService != null) { 215 | mATermService.currentTerminal.removeObservers(this); 216 | } 217 | 218 | if (mATermService != null) 219 | mATermService.mPreferences.unregisterOnSharedPreferenceChangeListener(mTermPreferChanged); 220 | 221 | mATermService = null; 222 | mTSConnection = null; 223 | EventBus.getDefault().unregister(this); 224 | 225 | } 226 | 227 | @Override 228 | protected void onNewIntent(Intent intent) { 229 | super.onNewIntent(intent); 230 | } 231 | 232 | @Override 233 | public void onConfigurationChanged(@NonNull Configuration newConfig) { 234 | super.onConfigurationChanged(newConfig); 235 | mHaveFullHwKeyboard = checkHaveFullHwKeyboard(newConfig); 236 | } 237 | 238 | private void updateTerminalPrefs(ATermSettings settings) { 239 | TerminalView view = this.mTerminalView; 240 | if (view == null) { 241 | return; 242 | } 243 | getWindowManager().getDefaultDisplay().getMetrics(metrics); 244 | 245 | view.setTextSize(Typeface.MONOSPACE, settings.getFontSize() * metrics.density); 246 | 247 | int[] scheme = settings.getColorScheme(); 248 | view.setDefaultColor(scheme[0], scheme[1]); 249 | 250 | view.setBackgroundAlpha(settings.getBackgroundAlpha()); 251 | 252 | } 253 | 254 | private boolean checkHaveFullHwKeyboard(Configuration c) { 255 | return (c.keyboard == Configuration.KEYBOARD_QWERTY) && 256 | (c.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO); 257 | } 258 | 259 | private void init() { 260 | if (mATermService != null) { 261 | //监听当前会话改变 262 | mATermService.currentTerminal.observe(this, terminal -> { 263 | if (terminal == null) { 264 | finish(); 265 | return; 266 | } 267 | 268 | if (mDrawerLayout != null) mDrawerLayout.closeDrawer(GravityCompat.START); 269 | 270 | initTerminalView(terminal); 271 | BaseApp.toastTop(terminal.getTitle()); 272 | }); 273 | mATermService.createTerminalIfNeed(); 274 | 275 | mATermService.mPreferences.registerOnSharedPreferenceChangeListener(mTermPreferChanged); 276 | 277 | RecyclerView termList = findViewById(R.id.term_list); 278 | termList.setAdapter(new TerminalListAdapter(mATermService)); 279 | 280 | } 281 | } 282 | 283 | 284 | private void doToggleSoftKeyboard() { 285 | InputMethodManager imm = (InputMethodManager) 286 | getSystemService(Context.INPUT_METHOD_SERVICE); 287 | if (imm != null) imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, 0); 288 | 289 | } 290 | 291 | private void initTerminalView(AbstractTerminal terminal) { 292 | if (mTerminalView == null) { 293 | return; 294 | } 295 | if (!(terminal instanceof AndroidTerminal) || !((AndroidTerminal) terminal).isExitAfterExit()) { 296 | terminal.setDestroyCallback((terminal1, exitCode) -> { 297 | handler.post(() -> { 298 | if (mATermService != null) { 299 | mATermService.removeTerminal(terminal1); 300 | } 301 | }); 302 | 303 | }); 304 | } 305 | if (mATermService != null) updateTerminalPrefs(mATermService.mSettings); 306 | 307 | mTerminalView.setTerminal(terminal); 308 | 309 | mTerminalView.setUpdateCallback(new UpdateCallback() { 310 | @Override 311 | public void onUpdate() { 312 | } 313 | 314 | @Override 315 | public void onBell() { 316 | if (bellUtil != null) bellUtil.doBell(); 317 | } 318 | }); 319 | 320 | mTerminalView.setModifiersChangedListener(modifiers -> mCtrlChecked.setChecked((modifiers & TerminalKeys.VTERM_MOD_CTRL) != 0)); 321 | 322 | 323 | } 324 | 325 | private void sendKeyCode(int keyCode) { 326 | KeyEvent[] events = {new KeyEvent(KeyEvent.ACTION_DOWN, keyCode), 327 | new KeyEvent(KeyEvent.ACTION_UP, keyCode) 328 | }; 329 | for (KeyEvent event : events) { 330 | dispatchKeyEvent(event); 331 | } 332 | } 333 | 334 | @Override 335 | public boolean dispatchKeyEvent(KeyEvent event) { 336 | if (mTerminalView != null) { 337 | AbstractTerminal terminal = mTerminalView.getTerminal(); 338 | if (terminal instanceof AndroidTerminal && ((AndroidTerminal) terminal).isExitAfterExit()) { 339 | ((AndroidTerminal) terminal).exitProcess(); 340 | } 341 | } 342 | return super.dispatchKeyEvent(event); 343 | } 344 | } 345 | 346 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermIntentHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | 7 | import androidx.appcompat.app.AppCompatActivity; 8 | 9 | public class ATermIntentHandler extends AppCompatActivity { 10 | public static final String TAG = "HandlerIntent"; 11 | 12 | @Override 13 | protected void onCreate(Bundle savedInstanceState) { 14 | super.onCreate(savedInstanceState); 15 | handleIntent(getIntent()); 16 | } 17 | 18 | @Override 19 | protected void onNewIntent(Intent intent) { 20 | super.onNewIntent(intent); 21 | handleIntent(intent); 22 | } 23 | 24 | private void handleIntent(Intent intent) { 25 | if (intent == null) { 26 | return; 27 | } 28 | // Log.d(TAG, "handlerIntent: " + intent); 29 | String action = intent.getAction(); 30 | if (ATermService.SERVICE_ACTION_CHANGE_DIRECTORY.equals(action)) { 31 | Intent intent1 = new Intent(this, ATermService.class); 32 | intent1.setAction(action); 33 | intent1.putExtras(intent); 34 | startService(intent1); 35 | } else if (ATermService.SERVICE_ACTION_EXEC_COMMAND.equals(action)) { 36 | Intent intent1 = new Intent(this, ATermService.class); 37 | intent1.setAction(action); 38 | intent1.putExtras(intent); 39 | startService(intent1); 40 | } else { 41 | try { 42 | Intent newIntent = new Intent(this, ATermService.class); 43 | newIntent.setDataAndType(intent.getData(), intent.getType()); 44 | if (intent.getExtras() != null) { 45 | newIntent.putExtras(intent); 46 | } 47 | startService(newIntent); 48 | } catch (Exception e) { 49 | Log.e(TAG, "handleIntent: ", e); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | protected void onResume() { 56 | super.onResume(); 57 | finish(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermPreferenceFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.preference.ListPreference; 6 | import androidx.preference.Preference; 7 | import androidx.preference.PreferenceFragmentCompat; 8 | 9 | 10 | public class ATermPreferenceFragment extends PreferenceFragmentCompat { 11 | 12 | private final Preference.OnPreferenceChangeListener mPreferenceChangeListener = (preference, newValue) -> { 13 | ListPreference listPreference1 = (ListPreference) preference; 14 | int index = listPreference1.findIndexOfValue(newValue.toString()); 15 | CharSequence[] entries = listPreference1.getEntries(); 16 | preference.setSummary(entries[index]); 17 | return true; 18 | }; 19 | 20 | @Override 21 | public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 22 | getPreferenceManager().setSharedPreferencesName(ATermService.PREFERENCES_NAME); 23 | addPreferencesFromResource(R.xml.aterm_preferences); 24 | setListPreferenceListener((ListPreference) findPreference(ATermSettings.FONTSIZE_KEY)); 25 | setListPreferenceListener((ListPreference) findPreference(ATermSettings.COLOR_KEY)); 26 | // setListPreferenceListener((ListPreference) findPreference(TermSettings.CONTROLKEY_KEY)); 27 | // setListPreferenceListener((ListPreference) findPreference(TermSettings.FNKEY_KEY)); 28 | setListPreferenceListener((ListPreference) findPreference(ATermSettings.TERMTYPE_KEY)); 29 | } 30 | 31 | private void setListPreferenceListener(ListPreference listPreference) { 32 | if (listPreference == null) { 33 | return; 34 | } 35 | CharSequence entry = listPreference.getEntry(); 36 | if (entry != null) { 37 | listPreference.setSummary(entry); 38 | } 39 | listPreference.setOnPreferenceChangeListener(mPreferenceChangeListener); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermService.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.app.NotificationChannel; 4 | import android.app.NotificationManager; 5 | import android.app.PendingIntent; 6 | import android.app.Service; 7 | import android.content.Context; 8 | import android.content.Intent; 9 | import android.content.SharedPreferences; 10 | import android.net.Uri; 11 | import android.os.Binder; 12 | import android.os.Build; 13 | import android.os.Handler; 14 | import android.os.IBinder; 15 | import android.text.TextUtils; 16 | import android.util.Log; 17 | import android.widget.Toast; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.core.app.NotificationCompat; 21 | import androidx.databinding.ObservableArrayList; 22 | import androidx.databinding.ObservableField; 23 | import androidx.databinding.ObservableList; 24 | import androidx.lifecycle.MutableLiveData; 25 | 26 | import com.github.maoabc.BaseApp; 27 | import com.github.maoabc.aterm.ssh.SshTerminal; 28 | import com.github.maoabc.util.FileUtils; 29 | import com.github.maoabc.util.MimeTypes; 30 | 31 | import org.greenrobot.eventbus.EventBus; 32 | 33 | import java.io.File; 34 | import java.io.FileOutputStream; 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.io.OutputStream; 38 | import java.nio.charset.StandardCharsets; 39 | 40 | import aterm.terminal.AbstractTerminal; 41 | 42 | public class ATermService extends Service { 43 | public static final String TAG = "ATermService"; 44 | 45 | private static int number = 1; 46 | 47 | public static final String PREFERENCES_NAME = "aterm_preferences"; 48 | private static final int RUNNING_NOTIFICATION = 1; 49 | public static final String SERVICE_ACTION_START_SERVICE = "aterm.action.startService"; 50 | 51 | public static final String SERVICE_ACTION_CHANGE_DIRECTORY = "aterm.action.changeDirectory"; 52 | 53 | public static final String SERVICE_ACTION_EXEC_COMMAND = "aterm.action.execCommand"; 54 | 55 | public static final String SERVICE_ACTION_SEND_COMMAND = "aterm.action.sendCommand"; 56 | 57 | public static final String SERVICE_ACTION_STOP_SERVICE = "aterm.action.stopService"; 58 | 59 | public static final String EXTRA_WORKING_DIRECTORY = "aterm.action.extra.pwd"; 60 | public static final String EXTRA_ARGUMENTS = "aterm.action.extra.arguments"; 61 | 62 | public static final String EXTRA_ENVIRONMENTS = "aterm.action.extra.environments"; 63 | 64 | public static final String EXTRA_COMMAND = "aterm.action.extra.command"; 65 | 66 | private Handler handler = new Handler(); 67 | 68 | public final ObservableList terminals = new ObservableArrayList<>(); 69 | 70 | 71 | //用于layout中判断当前终端 72 | public final ObservableField curTerminal = new ObservableField<>(); 73 | 74 | public final MutableLiveData currentTerminal = new MutableLiveData<>(); 75 | 76 | SharedPreferences mPreferences; 77 | ATermSettings mSettings; 78 | 79 | class TSBinder extends Binder { 80 | ATermService getService() { 81 | Log.i("TermService", "Activity binding to service"); 82 | return ATermService.this; 83 | } 84 | } 85 | 86 | private static synchronized int nextId() { 87 | return number++; 88 | } 89 | 90 | private final IBinder mTSBinder = new TSBinder(); 91 | 92 | public ATermService() { 93 | 94 | } 95 | 96 | 97 | public void createTerminalIfNeed() { 98 | if (!terminals.isEmpty()) { 99 | return; 100 | } 101 | AbstractTerminal terminal = createLocalTerminal(); 102 | terminal.start(); 103 | addTerminal(terminal); 104 | } 105 | 106 | private boolean hasTerm(String key) { 107 | for (AbstractTerminal terminal : terminals) { 108 | if (terminal.getKey().equals(key)) { 109 | return true; 110 | } 111 | 112 | } 113 | return false; 114 | } 115 | 116 | 117 | public SshTerminal createSshTerminal(String host, int port, String username, String password, String privateKey, String passphase) { 118 | String key; 119 | do { 120 | key = SshTerminal.TAG + username + nextId(); 121 | } while (hasTerm(key)); 122 | 123 | return new SshTerminal(mSettings, host, port, username, password, privateKey, passphase, key); 124 | } 125 | 126 | public AndroidTerminal createLocalTerminal() { 127 | String key; 128 | do { 129 | key = AndroidTerminal.TAG + nextId(); 130 | } while (hasTerm(key)); 131 | 132 | return new AndroidTerminal(mSettings, "/system/bin/sh", new String[]{"-"}, null, key, false); 133 | } 134 | 135 | public AndroidTerminal createTerminal(@NonNull String executePath, String[] args, String[] env) { 136 | int lastSlashIndex = executePath.lastIndexOf('/'); 137 | String processName = (lastSlashIndex == -1 ? executePath : executePath.substring(lastSlashIndex + 1)); 138 | String key; 139 | String suf = ""; 140 | int i = 1; 141 | do { 142 | key = processName + suf; 143 | suf = "" + i++; 144 | } while (hasTerm(key)); 145 | 146 | return new AndroidTerminal(mSettings, executePath, args, env, key, true); 147 | } 148 | 149 | public void addTerminal(AbstractTerminal terminal) { 150 | terminals.add(terminal); 151 | setCurrentTerminal(terminal); 152 | } 153 | 154 | public void setCurrentTerminal(AbstractTerminal terminal) { 155 | int currentItem = terminals.indexOf(terminal); 156 | setCurrentTerminal(currentItem); 157 | } 158 | 159 | private void setCurrentTerminal(int currentItem) { 160 | if (currentItem >= 0 && currentItem < terminals.size()) { 161 | AbstractTerminal terminal = terminals.get(currentItem); 162 | currentTerminal.setValue(terminal); 163 | curTerminal.set(terminal); 164 | } else { 165 | currentTerminal.setValue(null); 166 | curTerminal.set(null); 167 | } 168 | } 169 | 170 | public void removeTerminal(AbstractTerminal terminal) { 171 | int index = terminals.indexOf(terminal); 172 | if (index != -1) { 173 | terminals.remove(index); 174 | terminal.release(); 175 | AbstractTerminal currentTerminal = this.currentTerminal.getValue(); 176 | if (terminal.equals(currentTerminal)) { 177 | setCurrentTerminal(Math.min(index, terminals.size() - 1)); 178 | } else { 179 | setCurrentTerminal(terminals.indexOf(currentTerminal)); 180 | } 181 | } 182 | if (terminals.isEmpty()) { 183 | Intent intent = new Intent(this, ATermService.class); 184 | intent.setAction(ATermService.SERVICE_ACTION_STOP_SERVICE); 185 | startService(intent); 186 | } 187 | } 188 | 189 | 190 | @Override 191 | public IBinder onBind(Intent intent) { 192 | return mTSBinder; 193 | } 194 | 195 | @Override 196 | public int onStartCommand(Intent intent, int flags, int startId) { 197 | String action = ""; 198 | if (intent != null) { 199 | action = intent.getAction(); 200 | } 201 | if (SERVICE_ACTION_STOP_SERVICE.equals(action)) { 202 | stopSelf(); 203 | EventBus.getDefault().post(new FinishTerminalActivityEvent()); 204 | } else if (SERVICE_ACTION_CHANGE_DIRECTORY.equals(action)) { 205 | AbstractTerminal terminal = currentTerminal.getValue(); 206 | if (terminal == null) {//第一次启动可能没任何终端,先创建个本地终端,然后设置为当前 207 | terminal = createLocalTerminal(); 208 | terminal.start(); 209 | addTerminal(terminal); 210 | } 211 | 212 | try { 213 | byte result = 'u' - 'a' + '\001'; 214 | if (intent.hasExtra(EXTRA_WORKING_DIRECTORY)) { 215 | String dir = intent.getStringExtra(EXTRA_WORKING_DIRECTORY); 216 | if (!TextUtils.isEmpty(dir)) { 217 | byte[] b = {result}; 218 | terminal.writeToPty(b, 1); 219 | byte[] bytes = ("cd \"" + dir.trim() + "\"\n").getBytes(StandardCharsets.UTF_8); 220 | terminal.writeToPty(bytes, bytes.length); 221 | terminal.flushToPty(); 222 | } 223 | } 224 | } catch (Exception e) { 225 | } 226 | 227 | startActivity(new Intent(this, ATermActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 228 | 229 | } else if (SERVICE_ACTION_EXEC_COMMAND.equals(action)) { 230 | String[] args = intent.getStringArrayExtra(EXTRA_ARGUMENTS); 231 | if (args == null || args.length == 0) { 232 | Toast.makeText(getBaseContext(), "Arguments is empty", Toast.LENGTH_SHORT).show(); 233 | } else { 234 | String execute = args[0]; 235 | 236 | String[] env = intent.getStringArrayExtra(EXTRA_ENVIRONMENTS); 237 | 238 | // Log.d(TAG, "onStartCommand: exec command" + args); 239 | AndroidTerminal terminal = createTerminal(execute, args, env); 240 | terminal.setDestroyCallback(this::execFinishWaitKeyDown); 241 | terminal.start(); 242 | addTerminal(terminal); 243 | 244 | startActivity(new Intent(this, ATermActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 245 | } 246 | 247 | } else if (SERVICE_ACTION_START_SERVICE.equals(action)) { 248 | startActivity(new Intent(this, ATermActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 249 | } else if (SERVICE_ACTION_SEND_COMMAND.equals(action)) { 250 | AbstractTerminal terminal = currentTerminal.getValue(); 251 | if (terminal == null) {//第一次启动可能没任何终端,先创建个本地终端,然后设置为当前 252 | terminal = createLocalTerminal(); 253 | terminal.start(); 254 | addTerminal(terminal); 255 | } 256 | // Log.d(TAG, "onStartCommand: SEND command "+terminal); 257 | 258 | try { 259 | byte result = 'u' - 'a' + '\001'; 260 | if (intent.hasExtra(EXTRA_COMMAND)) { 261 | String cmd = intent.getStringExtra(EXTRA_COMMAND); 262 | if (!TextUtils.isEmpty(cmd)) { 263 | byte[] b = {result}; 264 | terminal.writeToPty(b, 1); 265 | byte[] bytes = (cmd.trim() + "\n").getBytes(StandardCharsets.UTF_8); 266 | terminal.writeToPty(bytes, bytes.length); 267 | terminal.flushToPty(); 268 | } 269 | } 270 | } catch (Exception e) { 271 | } 272 | startActivity(new Intent(this, ATermActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 273 | } else {//根据文件名执行脚本 274 | if (intent != null) { 275 | try { 276 | Uri data = intent.getData(); 277 | if (data != null && MimeTypes.isShellScript(data.getPath())) { 278 | String filePath = FileUtils.getFileFromUri(data); 279 | File tempFile; 280 | if (filePath == null) { 281 | tempFile = createTempFile(data); 282 | filePath = tempFile.getAbsolutePath(); 283 | } else { 284 | tempFile = null; 285 | } 286 | 287 | 288 | if (!TextUtils.isEmpty(filePath)) { 289 | String executePath = "/system/bin/sh"; 290 | String[] args = new String[3]; 291 | args[0] = executePath; 292 | args[1] = "-c"; 293 | args[2] = "sh " + filePath; 294 | 295 | AndroidTerminal terminal = createTerminal(executePath, args, null); 296 | terminal.setDestroyCallback((terminal1, exitCode) -> { 297 | if (tempFile != null) tempFile.delete(); 298 | execFinishWaitKeyDown(terminal1, exitCode); 299 | }); 300 | terminal.start(); 301 | 302 | addTerminal(terminal); 303 | 304 | startActivity(new Intent(this, ATermActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); 305 | } else { 306 | BaseApp.toast("Can't run"); 307 | } 308 | } 309 | } catch (Exception e) { 310 | BaseApp.toast(e.getLocalizedMessage()); 311 | } 312 | } 313 | } 314 | 315 | return START_STICKY; 316 | } 317 | 318 | private File createTempFile(Uri data) throws IOException { 319 | InputStream input = getContentResolver().openInputStream(data); 320 | File tempFile = File.createTempFile("script", "temp.sh"); 321 | FileOutputStream os = new FileOutputStream(tempFile); 322 | FileUtils.copyStreamAndClose(input, new OutputStream() { 323 | private long writeCount; 324 | 325 | @Override 326 | public void write(int b) throws IOException { 327 | os.write(b); 328 | writeCount++; 329 | checkOutsize(); 330 | } 331 | 332 | @Override 333 | public void flush() throws IOException { 334 | os.flush(); 335 | } 336 | 337 | @Override 338 | public void write(byte[] b) throws IOException { 339 | os.write(b); 340 | writeCount += b.length; 341 | checkOutsize(); 342 | } 343 | 344 | @Override 345 | public void write(byte[] b, int off, int len) throws IOException { 346 | os.write(b, off, len); 347 | writeCount += len; 348 | checkOutsize(); 349 | } 350 | 351 | @Override 352 | public void close() throws IOException { 353 | os.close(); 354 | } 355 | 356 | private void checkOutsize() throws IOException { 357 | if (writeCount > 1024 * 1024) { 358 | throw new IOException("File is too large"); 359 | } 360 | } 361 | }); 362 | return tempFile; 363 | } 364 | 365 | private void execFinishWaitKeyDown(AbstractTerminal terminal, int exitCode) { 366 | if (terminal instanceof AndroidTerminal) { 367 | AndroidTerminal androidTerminal = (AndroidTerminal) terminal; 368 | 369 | byte[] bytes = BaseApp.getResString(R.string.terminal_session_exit_msg, exitCode).getBytes(StandardCharsets.UTF_8); 370 | terminal.writeToPty(bytes, bytes.length); 371 | terminal.flushToPty(); 372 | //等待按键输入,然后执行完毕退出 373 | try { 374 | androidTerminal.waitKeyDown(); 375 | } catch (InterruptedException e) { 376 | e.printStackTrace(); 377 | } 378 | 379 | handler.post(() -> { 380 | removeTerminal(terminal); 381 | 382 | if (!terminals.isEmpty()) { 383 | EventBus.getDefault().post(new FinishTerminalActivityEvent()); 384 | } 385 | }); 386 | } 387 | } 388 | 389 | @Override 390 | public void onCreate() { 391 | super.onCreate(); 392 | Intent notifyIntent = new Intent(this, ATermActivity.class); 393 | notifyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 394 | PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notifyIntent, 0); 395 | 396 | String channel = createNotificationChannel(); 397 | 398 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channel) 399 | .setOngoing(true) 400 | .setWhen(System.currentTimeMillis()) 401 | .setSmallIcon(R.drawable.ic_stat_terminal) 402 | .setContentText(getString(R.string.term_session_running)) 403 | .setContentIntent(pendingIntent); 404 | Intent intent = new Intent(this, ATermService.class).setAction(SERVICE_ACTION_STOP_SERVICE); 405 | builder.addAction(android.R.drawable.ic_delete, getString(R.string.close), 406 | PendingIntent.getService(this, 0, intent, 0)); 407 | try { 408 | startForeground(RUNNING_NOTIFICATION, builder.build()); 409 | } catch (Exception e) { 410 | stopForeground(true); 411 | } 412 | 413 | mPreferences = getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 414 | mSettings = new ATermSettings(mPreferences); 415 | 416 | 417 | } 418 | 419 | private String createNotificationChannel() { 420 | String channelId = ""; 421 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 422 | channelId = "term sessions"; 423 | NotificationChannel channel = new NotificationChannel(channelId, "term sessions", NotificationManager.IMPORTANCE_NONE); 424 | NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 425 | if (manager != null) manager.createNotificationChannel(channel); 426 | } 427 | return channelId; 428 | } 429 | 430 | public boolean onTerminalItemLongClick(AbstractTerminal terminal) { 431 | EventBus.getDefault().post(new TerminalLongClickEvent(terminal)); 432 | return true; 433 | } 434 | 435 | @Override 436 | public void onDestroy() { 437 | super.onDestroy(); 438 | stopForeground(true); 439 | 440 | for (AbstractTerminal terminal : terminals) { 441 | terminal.release(); 442 | } 443 | terminals.clear(); 444 | currentTerminal.setValue(null); 445 | curTerminal.set(null); 446 | BaseApp.toast(R.string.closed_term_session); 447 | } 448 | 449 | public static class TerminalLongClickEvent { 450 | public final AbstractTerminal terminal; 451 | 452 | TerminalLongClickEvent(AbstractTerminal terminal) { 453 | this.terminal = terminal; 454 | } 455 | } 456 | 457 | public static class FinishTerminalActivityEvent { 458 | FinishTerminalActivityEvent() { 459 | } 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermSettings.java: -------------------------------------------------------------------------------- 1 | 2 | package com.github.maoabc.aterm; 3 | 4 | import android.content.SharedPreferences; 5 | import android.text.TextUtils; 6 | import android.util.Log; 7 | import android.view.KeyEvent; 8 | 9 | /** 10 | * Terminal emulator settings 11 | */ 12 | public class ATermSettings { 13 | 14 | private int mOrientation; 15 | private int mFontSize; 16 | private int mColorId; 17 | private int mBackKeyAction; 18 | private int mControlKeyId; 19 | private int mFnKeyId; 20 | private int mUseCookedIME; 21 | private String mShell; 22 | // private String mInitialCommand; 23 | // private String mTermType; 24 | // private boolean mCloseOnExit; 25 | // private boolean mVerifyPath; 26 | // private String mHomePath; 27 | 28 | private int mBackgroundAlpha = 0xff; 29 | 30 | private boolean mAltSendsEsc; 31 | 32 | private boolean mMouseTracking; 33 | 34 | private boolean mUseKeyboardShortcuts; 35 | 36 | private static final String ORIENTATION_KEY = "orientation"; 37 | public static final String FONTSIZE_KEY = "fontsize"; 38 | public static final String COLOR_KEY = "color"; 39 | private static final String BACKACTION_KEY = "backaction"; 40 | public static final String CONTROLKEY_KEY = "controlkey"; 41 | public static final String FNKEY_KEY = "fnkey"; 42 | private static final String IME_KEY = "ime"; 43 | private static final String SHELL_KEY = "shell"; 44 | private static final String INITIALCOMMAND_KEY = "initialcommand"; 45 | public static final String TERMTYPE_KEY = "termtype"; 46 | private static final String CLOSEONEXIT_KEY = "close_window_on_process_exit"; 47 | private static final String VERIFYPATH_KEY = "verify_path"; 48 | private static final String PATHEXTENSIONS_KEY = "do_path_extensions"; 49 | private static final String PATHPREPEND_KEY = "allow_prepend_path"; 50 | private static final String HOMEPATH_KEY = "home_path"; 51 | private static final String ALT_SENDS_ESC = "alt_sends_esc"; 52 | private static final String MOUSE_TRACKING = "mouse_tracking"; 53 | private static final String USE_KEYBOARD_SHORTCUTS = "use_keyboard_shortcuts"; 54 | public static final String BACKGROUND_ALPHA = "background_alpha"; 55 | 56 | public static final int WHITE = 0xffffffff; 57 | public static final int BLACK = 0xff000000; 58 | public static final int BLUE = 0xff344ebd; 59 | public static final int GREEN = 0xff00ff00; 60 | public static final int AMBER = 0xffffb651; 61 | public static final int RED = 0xffff0113; 62 | public static final int HOLO_BLUE = 0xff33b5e5; 63 | public static final int SOLARIZED_FG = 0xff657b83; 64 | public static final int SOLARIZED_BG = 0xfffdf6e3; 65 | public static final int SOLARIZED_DARK_FG = 0xff839496; 66 | public static final int SOLARIZED_DARK_BG = 0xff002b36; 67 | public static final int LINUX_CONSOLE_WHITE = 0xffaaaaaa; 68 | 69 | // foreground color, background color 70 | private static final int[][] COLOR_SCHEMES = { 71 | {BLACK, WHITE}, 72 | {WHITE, BLACK}, 73 | {WHITE, BLUE}, 74 | {GREEN, BLACK}, 75 | {AMBER, BLACK}, 76 | {RED, BLACK}, 77 | {HOLO_BLUE, BLACK}, 78 | {SOLARIZED_FG, SOLARIZED_BG}, 79 | {SOLARIZED_DARK_FG, SOLARIZED_DARK_BG}, 80 | {LINUX_CONSOLE_WHITE, BLACK} 81 | }; 82 | 83 | 84 | public static final int ORIENTATION_UNSPECIFIED = 0; 85 | public static final int ORIENTATION_LANDSCAPE = 1; 86 | public static final int ORIENTATION_PORTRAIT = 2; 87 | 88 | /** 89 | * An integer not in the range of real key codes. 90 | */ 91 | public static final int KEYCODE_NONE = -1; 92 | 93 | public static final int CONTROL_KEY_ID_NONE = 7; 94 | public static final int[] CONTROL_KEY_SCHEMES = { 95 | KeyEvent.KEYCODE_DPAD_CENTER, 96 | KeyEvent.KEYCODE_AT, 97 | KeyEvent.KEYCODE_ALT_LEFT, 98 | KeyEvent.KEYCODE_ALT_RIGHT, 99 | KeyEvent.KEYCODE_VOLUME_UP, 100 | KeyEvent.KEYCODE_VOLUME_DOWN, 101 | KeyEvent.KEYCODE_CAMERA, 102 | KEYCODE_NONE 103 | }; 104 | 105 | public static final int FN_KEY_ID_NONE = 7; 106 | public static final int[] FN_KEY_SCHEMES = { 107 | KeyEvent.KEYCODE_DPAD_CENTER, 108 | KeyEvent.KEYCODE_AT, 109 | KeyEvent.KEYCODE_ALT_LEFT, 110 | KeyEvent.KEYCODE_ALT_RIGHT, 111 | KeyEvent.KEYCODE_VOLUME_UP, 112 | KeyEvent.KEYCODE_VOLUME_DOWN, 113 | KeyEvent.KEYCODE_CAMERA, 114 | KEYCODE_NONE 115 | }; 116 | 117 | public static final int BACK_KEY_STOPS_SERVICE = 0; 118 | public static final int BACK_KEY_CLOSES_WINDOW = 1; 119 | public static final int BACK_KEY_CLOSES_ACTIVITY = 2; 120 | public static final int BACK_KEY_SENDS_ESC = 3; 121 | public static final int BACK_KEY_SENDS_TAB = 4; 122 | private static final int BACK_KEY_MAX = 4; 123 | 124 | public ATermSettings(SharedPreferences prefs) { 125 | readPrefs(prefs); 126 | } 127 | 128 | 129 | public void readPrefs(SharedPreferences prefs) { 130 | mOrientation = get(prefs, ORIENTATION_KEY, ORIENTATION_PORTRAIT); 131 | mFontSize = get(prefs, FONTSIZE_KEY, 14); 132 | mColorId = get(prefs, COLOR_KEY, 1); 133 | mBackKeyAction = get(prefs, BACKACTION_KEY, 2); 134 | mControlKeyId = get(prefs, CONTROLKEY_KEY, 5); 135 | mFnKeyId = get(prefs, FNKEY_KEY, 4); 136 | mUseCookedIME = get(prefs, IME_KEY, 0); 137 | mAltSendsEsc = get(prefs, ALT_SENDS_ESC, false); 138 | mShell = get(prefs, SHELL_KEY, "/system/bin/sh -"); 139 | // mInitialCommand = get(prefs, INITIALCOMMAND_KEY, ""); 140 | // mTermType = get(prefs, TERMTYPE_KEY, "xterm-256color"); 141 | // mCloseOnExit = get(prefs, CLOSEONEXIT_KEY, true); 142 | // mVerifyPath = get(prefs, VERIFYPATH_KEY, true); 143 | mMouseTracking = get(prefs, MOUSE_TRACKING, mMouseTracking); 144 | mUseKeyboardShortcuts = get(prefs, USE_KEYBOARD_SHORTCUTS, false); 145 | // mHomePath = get(prefs, HOMEPATH_KEY, "/"); 146 | 147 | mBackgroundAlpha = prefs.getInt(BACKGROUND_ALPHA, 0xff); 148 | 149 | } 150 | 151 | public int get(SharedPreferences prefs, String key, int defValue) { 152 | String v = prefs.getString(key, ""); 153 | if (TextUtils.isEmpty(v)) { 154 | return defValue; 155 | } 156 | try { 157 | return Integer.parseInt(v); 158 | } catch (NumberFormatException e) { 159 | return defValue; 160 | } 161 | } 162 | 163 | public boolean get(SharedPreferences prefs, String key, boolean defValue) { 164 | return prefs.getBoolean(key, defValue); 165 | } 166 | 167 | public String get(SharedPreferences prefs, String key, String defValue) { 168 | return prefs.getString(key, defValue); 169 | } 170 | 171 | 172 | public int getScreenOrientation() { 173 | return mOrientation; 174 | } 175 | 176 | 177 | public int getFontSize() { 178 | return mFontSize; 179 | } 180 | 181 | public int[] getColorScheme() { 182 | Log.d("Aterm setting", "getColorScheme: " + mColorId); 183 | if (mColorId >= 0 && mColorId < COLOR_SCHEMES.length) { 184 | return COLOR_SCHEMES[mColorId]; 185 | } else { 186 | 187 | return COLOR_SCHEMES[1]; 188 | } 189 | } 190 | 191 | 192 | public int getBackKeyAction() { 193 | return mBackKeyAction; 194 | } 195 | 196 | public boolean backKeySendsCharacter() { 197 | return mBackKeyAction >= BACK_KEY_SENDS_ESC; 198 | } 199 | 200 | public boolean getAltSendsEscFlag() { 201 | return mAltSendsEsc; 202 | } 203 | 204 | public boolean getMouseTrackingFlag() { 205 | return mMouseTracking; 206 | } 207 | 208 | public boolean getUseKeyboardShortcutsFlag() { 209 | return mUseKeyboardShortcuts; 210 | } 211 | 212 | public int getBackKeyCharacter() { 213 | switch (mBackKeyAction) { 214 | case BACK_KEY_SENDS_ESC: 215 | return 27; 216 | case BACK_KEY_SENDS_TAB: 217 | return 9; 218 | default: 219 | return 0; 220 | } 221 | } 222 | 223 | public int getControlKeyId() { 224 | return mControlKeyId; 225 | } 226 | 227 | public int getFnKeyId() { 228 | return mFnKeyId; 229 | } 230 | 231 | public int getControlKeyCode() { 232 | if (mControlKeyId >= 0 && mControlKeyId < CONTROL_KEY_SCHEMES.length) { 233 | return CONTROL_KEY_SCHEMES[mControlKeyId]; 234 | } else { 235 | return CONTROL_KEY_SCHEMES[CONTROL_KEY_ID_NONE]; 236 | } 237 | } 238 | 239 | public int getFnKeyCode() { 240 | if (mFnKeyId >= 0 && mFnKeyId < FN_KEY_SCHEMES.length) { 241 | return FN_KEY_SCHEMES[mFnKeyId]; 242 | } else { 243 | return FN_KEY_SCHEMES[FN_KEY_ID_NONE]; 244 | } 245 | } 246 | 247 | public boolean useCookedIME() { 248 | return (mUseCookedIME != 0); 249 | } 250 | 251 | public String getShell() { 252 | if (TextUtils.isEmpty(mShell)) { 253 | return "/system/bin/sh -"; 254 | } 255 | return mShell; 256 | } 257 | 258 | public int getBackgroundAlpha() { 259 | return mBackgroundAlpha; 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ATermSettingsActivity.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.appcompat.app.AppCompatActivity; 6 | import androidx.appcompat.widget.Toolbar; 7 | 8 | public class ATermSettingsActivity extends AppCompatActivity { 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_aterm_settings); 14 | 15 | Toolbar toolbar = findViewById(R.id.toolbar); 16 | setSupportActionBar(toolbar); 17 | 18 | toolbar.setNavigationOnClickListener(v -> onBackPressed()); 19 | if (savedInstanceState == null) { 20 | getSupportFragmentManager() 21 | .beginTransaction() 22 | .replace(R.id.preference_fragment, new ATermPreferenceFragment()) 23 | .commit(); 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/AndroidTerminal.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.text.TextUtils; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | 8 | import java.io.File; 9 | import java.io.FileDescriptor; 10 | import java.io.FileInputStream; 11 | import java.io.FileOutputStream; 12 | import java.io.IOException; 13 | import java.util.Arrays; 14 | import java.util.concurrent.CountDownLatch; 15 | 16 | import aterm.pty.Pty; 17 | import aterm.terminal.AbstractTerminal; 18 | 19 | public class AndroidTerminal extends AbstractTerminal { 20 | static final String TAG = "Local"; 21 | private static final String ROOT = "/data/data/com.github.maoabc.aterm"; 22 | private static final boolean DEBUG = BuildConfig.DEBUG; 23 | 24 | private final ATermSettings mSettings; 25 | private final String mKey; 26 | private final String mExecutePath; 27 | private final String[] mArgs; 28 | private final String[] mEnv; 29 | private String mTitle; 30 | 31 | //执行结束退出 32 | private final boolean mExitAfterExec; 33 | 34 | private volatile CountDownLatch mLatch; 35 | 36 | private int mMasterFd = -1; 37 | private FileInputStream mPtyInput; 38 | private FileOutputStream mPtyOutput; 39 | private int mPid; 40 | 41 | public AndroidTerminal(@NonNull ATermSettings settings, @NonNull String executePath, String[] args, String[] env, @NonNull String key, boolean exit) { 42 | super(10, 50, 1000, settings.getColorScheme()[0], settings.getColorScheme()[1]); 43 | mSettings = settings; 44 | this.mEnv = env; 45 | mKey = key; 46 | mTitle = key; 47 | this.mExecutePath = executePath; 48 | this.mArgs = args; 49 | this.mExitAfterExec = exit; 50 | } 51 | 52 | 53 | public void start() { 54 | 55 | if (DEBUG) Log.d(TAG, "start: " + getKey()); 56 | 57 | try { 58 | 59 | exec(); 60 | 61 | new Thread("Terminal reader") { 62 | @Override 63 | public void run() { 64 | try { 65 | byte[] bytes = new byte[4096]; 66 | while (true) { 67 | int len = mPtyInput.read(bytes, 0, bytes.length); 68 | if (len == 0) { 69 | continue; 70 | } 71 | if (len == -1) { 72 | break; 73 | } 74 | if (DEBUG) Log.d(TAG, "run: " + new String(bytes, 0, len)); 75 | //write to terminal 76 | inputWrite(bytes, 0, len); 77 | } 78 | } catch (IOException e) { 79 | Log.e(TAG, "read from pty: ", e); 80 | } 81 | } 82 | }.start(); 83 | } catch (IOException e) { 84 | Log.e(TAG, "start: ", e); 85 | } 86 | new Thread("Wait for") { 87 | @Override 88 | public void run() { 89 | int code = waitFor(); 90 | if (DEBUG) Log.d(TAG, "wait: " + code); 91 | if (mDestroyCallback != null) 92 | mDestroyCallback.onDestroy(AndroidTerminal.this, code); 93 | closePty(); 94 | } 95 | }.start(); 96 | } 97 | 98 | private void exec() throws IOException { 99 | final String shell = TextUtils.isEmpty(mExecutePath) ? "/system/bin/sh" : mExecutePath; 100 | 101 | final int[] masterFd = new int[1]; 102 | mPid = Pty.exec(shell, mArgs, initEnv(), 103 | 25, 30, masterFd); 104 | mMasterFd = masterFd[0]; 105 | FileDescriptor fd = Pty.createFileDescriptor(mMasterFd); 106 | mPtyInput = new FileInputStream(fd); 107 | mPtyOutput = new FileOutputStream(fd); 108 | } 109 | 110 | public boolean isExitAfterExit() { 111 | return mExitAfterExec; 112 | } 113 | 114 | 115 | public void exitProcess() { 116 | if (mLatch != null) mLatch.countDown(); 117 | } 118 | 119 | public void waitKeyDown() throws InterruptedException { 120 | if (mLatch == null) { 121 | mLatch = new CountDownLatch(1); 122 | } 123 | mLatch.await(); 124 | } 125 | 126 | //生成环境变量 127 | private String[] initEnv() { 128 | 129 | String[] extEnv = this.mEnv; 130 | int extEnvLength = 0; 131 | if (extEnv != null) { 132 | extEnvLength = extEnv.length; 133 | } 134 | 135 | String[] env = new String[extEnvLength + 3]; 136 | int i = 0; 137 | for (; i < extEnvLength; i++) { 138 | env[i] = extEnv[i]; 139 | } 140 | 141 | 142 | String path = System.getenv("PATH"); 143 | env[i++] = "TERM=xterm-256color"; 144 | env[i++] = "PATH=" + ROOT + "/bin:" + path; 145 | env[i] = "HOME=" + ROOT; 146 | 147 | if (DEBUG) Log.d(TAG, "initEnv: " + Arrays.toString(env)); 148 | 149 | return env; 150 | } 151 | 152 | //检测路径是否可执行 153 | private String checkPath(String path) { 154 | if (path == null) { 155 | return ""; 156 | } 157 | String[] dirs = path.split(":"); 158 | StringBuilder checkedPath = new StringBuilder(path.length()); 159 | for (String dirname : dirs) { 160 | File dir = new File(dirname); 161 | if (dir.isDirectory() && dir.canExecute()) { 162 | checkedPath.append(dirname); 163 | checkedPath.append(":"); 164 | } 165 | } 166 | return checkedPath.substring(0, checkedPath.length() - 1); 167 | } 168 | 169 | @NonNull 170 | @Override 171 | public String getTitle() { 172 | return mTitle; 173 | } 174 | 175 | @Override 176 | public void setTitle(@NonNull String title) { 177 | this.mTitle = title; 178 | } 179 | 180 | @NonNull 181 | @Override 182 | public String getKey() { 183 | return mKey; 184 | } 185 | 186 | @Override 187 | protected void setPtyWindowSize(int cols, int rows) { 188 | try { 189 | Pty.setWindowSize(mMasterFd, rows, cols); 190 | } catch (IOException e) { 191 | Log.e(TAG, "setPtyWindowSize: " + getKey(), e); 192 | } 193 | 194 | } 195 | 196 | @Override 197 | protected void closePty() { 198 | try { 199 | Pty.close(mMasterFd); 200 | } catch (IOException e) { 201 | Log.e(TAG, "closePty: ", e); 202 | } 203 | } 204 | 205 | private int waitFor() { 206 | return Pty.waitFor(mPid); 207 | } 208 | 209 | @Override 210 | protected int scrollRowSize() { 211 | //todo 通过外部设置 212 | return 1000; 213 | } 214 | 215 | @Override 216 | public void flushToPty() { 217 | 218 | } 219 | 220 | @Override 221 | public void release() { 222 | } 223 | 224 | @Override 225 | public void writeToPty(final byte[] bytes, final int len) { 226 | //data write to pty 227 | try { 228 | // if (DEBUG) Log.d(TAG, "writeToPty: " + new String(HexEncoding.encode(bytes, 0, len))); 229 | if (mPtyOutput != null) mPtyOutput.write(bytes, 0, len); 230 | } catch (Exception e) { 231 | Log.e(TAG, "outputWrite: ", e); 232 | } 233 | } 234 | 235 | } 236 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/BellUtil.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | import android.os.Message; 7 | import android.os.SystemClock; 8 | import android.os.Vibrator; 9 | 10 | public class BellUtil { 11 | private static BellUtil instance = null; 12 | private final Vibrator mVibrator; 13 | public static final int MSG_ID = 6555; 14 | 15 | public static BellUtil getInstance(Context context) { 16 | if (instance == null) { 17 | synchronized (BellUtil.class) { 18 | if (instance == null) { 19 | instance = new BellUtil(context); 20 | } 21 | } 22 | } 23 | 24 | return instance; 25 | } 26 | 27 | private static final int DURATION = 30; 28 | 29 | private final Handler handler = new Handler(Looper.getMainLooper()) { 30 | @Override 31 | public void handleMessage(Message msg) { 32 | if (msg.what == MSG_ID) { 33 | vibrator(); 34 | } 35 | } 36 | }; 37 | 38 | private void vibrator() { 39 | if (mVibrator != null) { 40 | mVibrator.vibrate(DURATION); 41 | } 42 | } 43 | 44 | private long lastBell = 0; 45 | 46 | 47 | private BellUtil(Context context) { 48 | mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); 49 | } 50 | 51 | public synchronized void doBell() { 52 | long now = SystemClock.uptimeMillis(); 53 | long timeSinceLastBell = now - lastBell; 54 | 55 | if (timeSinceLastBell > 0) { 56 | if (timeSinceLastBell < 3 * DURATION) { 57 | handler.sendEmptyMessageDelayed(MSG_ID, 3 * DURATION - timeSinceLastBell); 58 | lastBell = lastBell + 3 * DURATION; 59 | } else { 60 | vibrator(); 61 | lastBell = now; 62 | } 63 | } 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/CreateSshServerDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | import android.text.Editable; 8 | import android.text.TextUtils; 9 | import android.text.TextWatcher; 10 | import android.view.LayoutInflater; 11 | import android.widget.Button; 12 | 13 | import androidx.annotation.NonNull; 14 | import androidx.annotation.Nullable; 15 | import androidx.annotation.StringRes; 16 | import androidx.appcompat.app.AlertDialog; 17 | import androidx.databinding.DataBindingUtil; 18 | import androidx.fragment.app.DialogFragment; 19 | import androidx.fragment.app.FragmentActivity; 20 | 21 | import com.github.maoabc.BaseApp; 22 | import com.github.maoabc.aterm.databinding.DialogCreateSshServerBinding; 23 | import com.github.maoabc.aterm.viewmodel.SshServerOption; 24 | import com.github.maoabc.util.Precondition; 25 | import com.jcraft.jsch.JSch; 26 | import com.jcraft.jsch.JSchException; 27 | 28 | public class CreateSshServerDialogFragment extends DialogFragment { 29 | 30 | private static final String TITLE_KEY = "title_key"; 31 | private static final String KEY_OPTIONS = "key_options"; 32 | 33 | private DialogCreateSshServerBinding mBinding; 34 | 35 | private String mTitle; 36 | 37 | private SshServerOption mServerOption; 38 | 39 | private ResultCallback mResultCallback; 40 | 41 | 42 | public static CreateSshServerDialogFragment newInstance(@StringRes int titleId, SshServerOption options) { 43 | 44 | Bundle args = new Bundle(); 45 | 46 | CreateSshServerDialogFragment fragment = new CreateSshServerDialogFragment(); 47 | args.putInt(TITLE_KEY, titleId); 48 | args.putParcelable(KEY_OPTIONS, options); 49 | 50 | fragment.setArguments(args); 51 | return fragment; 52 | } 53 | 54 | @Override 55 | public void onAttach(@NonNull Context context) { 56 | super.onAttach(context); 57 | 58 | Bundle args = getArguments(); 59 | Precondition.checkNotNull(args); 60 | 61 | mTitle = getString(args.getInt(TITLE_KEY, R.string.new_ssh_server)); 62 | mServerOption = args.getParcelable(KEY_OPTIONS); 63 | } 64 | 65 | public void setResultCallback(ResultCallback callback) { 66 | this.mResultCallback = callback; 67 | } 68 | 69 | @NonNull 70 | @Override 71 | public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 72 | FragmentActivity context = getActivity(); 73 | Precondition.checkNotNull(context); 74 | 75 | mBinding = DataBindingUtil.inflate(LayoutInflater.from(context), R.layout.dialog_create_ssh_server, null, false); 76 | mBinding.etHost.addTextChangedListener(new TextWatcher() { 77 | @Override 78 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 79 | 80 | } 81 | 82 | @Override 83 | public void onTextChanged(CharSequence s, int start, int before, int count) { 84 | mBinding.tlHost.setError(null); 85 | mBinding.tlHost.setErrorEnabled(false); 86 | } 87 | 88 | @Override 89 | public void afterTextChanged(Editable s) { 90 | 91 | } 92 | }); 93 | mBinding.etUsername.addTextChangedListener(new TextWatcher() { 94 | @Override 95 | public void beforeTextChanged(CharSequence s, int start, int count, int after) { 96 | 97 | } 98 | 99 | @Override 100 | public void onTextChanged(CharSequence s, int start, int before, int count) { 101 | mBinding.tlUsername.setError(null); 102 | mBinding.tlUsername.setErrorEnabled(false); 103 | } 104 | 105 | @Override 106 | public void afterTextChanged(Editable s) { 107 | 108 | } 109 | }); 110 | 111 | mBinding.setOption(mServerOption); 112 | 113 | AlertDialog alertDialog = new AlertDialog.Builder(context) 114 | .setTitle(mTitle) 115 | .setView(mBinding.getRoot()) 116 | .setPositiveButton(R.string.ok, null) 117 | .setNegativeButton(R.string.cancel, null) 118 | .create(); 119 | 120 | alertDialog.setOnShowListener(dialog -> { 121 | AlertDialog ad = (AlertDialog) dialog; 122 | Button positiveButton = ad.getButton(DialogInterface.BUTTON_POSITIVE); 123 | positiveButton.setOnClickListener(v -> { 124 | checkValid(); 125 | }); 126 | }); 127 | return alertDialog; 128 | } 129 | 130 | private void checkValid() { 131 | if (mServerOption == null) { 132 | return; 133 | } 134 | String host = mServerOption.getHost().toString(); 135 | if (TextUtils.isEmpty(host)) { 136 | mBinding.tlHost.setErrorEnabled(true); 137 | mBinding.tlHost.setError(getString(R.string.empty_host)); 138 | return; 139 | } 140 | String username = mServerOption.getUsername().toString(); 141 | if (TextUtils.isEmpty(username)) { 142 | mBinding.tlUsername.setErrorEnabled(true); 143 | mBinding.tlUsername.setError(getString(R.string.empty_username)); 144 | return; 145 | } 146 | if (TextUtils.isEmpty(mServerOption.getPassword()) && TextUtils.isEmpty(mServerOption.getPrivateKey())) { 147 | BaseApp.toast(R.string.need_enter_password_or_key); 148 | return; 149 | } 150 | if (!TextUtils.isEmpty(mServerOption.getPrivateKey())) { 151 | try { 152 | JSch jSch = new JSch(); 153 | final byte[] phassphrase; 154 | CharSequence sequence = mServerOption.getPassphrase(); 155 | if (TextUtils.isEmpty(sequence)) { 156 | phassphrase = null; 157 | } else { 158 | phassphrase = sequence.toString().getBytes(); 159 | } 160 | jSch.addIdentity("", mServerOption.getPrivateKey().toString().getBytes(), null, phassphrase); 161 | } catch (JSchException e) { 162 | BaseApp.toast(R.string.key_invalid); 163 | return; 164 | } 165 | } 166 | 167 | if (mResultCallback != null) mResultCallback.onResult(mServerOption); 168 | dismiss(); 169 | } 170 | 171 | 172 | @Override 173 | public void onDetach() { 174 | super.onDetach(); 175 | mResultCallback = null; 176 | } 177 | 178 | public interface ResultCallback { 179 | void onResult(SshServerOption option); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/TerminalListAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | 4 | import android.view.LayoutInflater; 5 | import android.view.ViewGroup; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.databinding.DataBindingUtil; 9 | import androidx.databinding.ObservableList; 10 | import androidx.recyclerview.widget.RecyclerView; 11 | 12 | import com.github.maoabc.aterm.databinding.TerminalListItemBinding; 13 | import com.github.maoabc.util.RecyclerViewAdapterChangedCallback; 14 | 15 | import java.util.List; 16 | 17 | import aterm.terminal.AbstractTerminal; 18 | 19 | public class TerminalListAdapter extends RecyclerView.Adapter { 20 | private final ATermService service; 21 | private List terminals; 22 | private RecyclerViewAdapterChangedCallback mListChangedCallback; 23 | 24 | TerminalListAdapter(ATermService service) { 25 | this.service = service; 26 | setList(service.terminals); 27 | } 28 | 29 | public void setList(List terminals) { 30 | if (this.terminals == terminals) { 31 | return; 32 | } 33 | if (this.terminals instanceof ObservableList) { 34 | //noinspection unchecked 35 | ((ObservableList) this.terminals).removeOnListChangedCallback(mListChangedCallback); 36 | } 37 | this.terminals = terminals; 38 | if (this.terminals instanceof ObservableList) { 39 | if (mListChangedCallback == null) { 40 | mListChangedCallback = new RecyclerViewAdapterChangedCallback(this); 41 | } 42 | //noinspection unchecked 43 | ((ObservableList) this.terminals).addOnListChangedCallback(mListChangedCallback); 44 | } 45 | notifyDataSetChanged(); 46 | } 47 | 48 | @NonNull 49 | @Override 50 | public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 51 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 52 | TerminalListItemBinding binding = DataBindingUtil.inflate(inflater, R.layout.terminal_list_item, parent, false); 53 | 54 | return new TextItemViewHolder(binding); 55 | } 56 | 57 | @Override 58 | public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { 59 | if (holder instanceof TextItemViewHolder) { 60 | AbstractTerminal terminal = terminals.get(holder.getLayoutPosition()); 61 | 62 | TerminalListItemBinding binding = ((TextItemViewHolder) holder).binding; 63 | 64 | binding.setService(service); 65 | 66 | binding.setTerminal(terminal); 67 | 68 | binding.executePendingBindings(); 69 | } 70 | 71 | } 72 | 73 | @Override 74 | public int getItemCount() { 75 | return terminals == null ? 0 : terminals.size(); 76 | } 77 | 78 | @Override 79 | public long getItemId(int position) { 80 | return position; 81 | } 82 | 83 | private static class TextItemViewHolder extends RecyclerView.ViewHolder { 84 | private final TerminalListItemBinding binding; 85 | 86 | TextItemViewHolder(@NonNull TerminalListItemBinding binding) { 87 | super(binding.getRoot()); 88 | this.binding = binding; 89 | } 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/TerminalManagerDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm; 2 | 3 | import android.app.Dialog; 4 | import android.content.DialogInterface; 5 | import android.os.Bundle; 6 | import android.text.TextUtils; 7 | import android.view.LayoutInflater; 8 | import android.view.ViewGroup; 9 | import android.widget.Button; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.Nullable; 13 | import androidx.appcompat.app.AlertDialog; 14 | import androidx.databinding.DataBindingUtil; 15 | import androidx.databinding.ObservableList; 16 | import androidx.fragment.app.DialogFragment; 17 | import androidx.fragment.app.FragmentActivity; 18 | import androidx.lifecycle.ViewModelProvider; 19 | import androidx.recyclerview.widget.RecyclerView; 20 | 21 | import com.github.maoabc.aterm.databinding.ListItemBinding; 22 | import com.github.maoabc.aterm.db.entities.SshServer; 23 | import com.github.maoabc.aterm.viewmodel.TerminalManagerViewModel; 24 | import com.github.maoabc.aterm.viewmodel.SshServerOption; 25 | import com.github.maoabc.aterm.viewmodel.TerminalItem; 26 | import com.github.maoabc.util.Precondition; 27 | import com.github.maoabc.util.RecyclerViewAdapterChangedCallback; 28 | 29 | import org.greenrobot.eventbus.EventBus; 30 | import org.greenrobot.eventbus.Subscribe; 31 | 32 | import java.util.List; 33 | 34 | public class TerminalManagerDialogFragment extends DialogFragment { 35 | 36 | private TerminalManagerViewModel mViewModel; 37 | 38 | public static TerminalManagerDialogFragment newInstance() { 39 | 40 | Bundle args = new Bundle(); 41 | 42 | TerminalManagerDialogFragment fragment = new TerminalManagerDialogFragment(); 43 | fragment.setArguments(args); 44 | return fragment; 45 | } 46 | 47 | @Override 48 | public void onCreate(@Nullable Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | mViewModel = new ViewModelProvider(this).get(TerminalManagerViewModel.class); 51 | EventBus.getDefault().register(this); 52 | } 53 | 54 | @NonNull 55 | @Override 56 | public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { 57 | 58 | final FragmentActivity context = getActivity(); 59 | Precondition.checkNotNull(context); 60 | 61 | LayoutInflater inflater = LayoutInflater.from(context); 62 | RecyclerView recyclerView = (RecyclerView) inflater.inflate(R.layout.recycler_view, null); 63 | recyclerView.setAdapter(new ServerItemAdapter(mViewModel)); 64 | 65 | 66 | AlertDialog alertDialog = new AlertDialog.Builder(context) 67 | .setTitle(R.string.new_terminal) 68 | .setView(recyclerView) 69 | .setPositiveButton(R.string.close, null) 70 | .setNeutralButton(R.string.new_ssh_server, null) 71 | .create(); 72 | alertDialog.setOnShowListener(dialog -> { 73 | Button neutral = alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL); 74 | if (neutral != null) { 75 | neutral.setOnClickListener(v -> { 76 | CreateSshServerDialogFragment fragment = CreateSshServerDialogFragment.newInstance(R.string.new_ssh_server, new SshServerOption()); 77 | fragment.setResultCallback(this::addServer); 78 | fragment.show(getParentFragmentManager(), null); 79 | }); 80 | } 81 | }); 82 | return alertDialog; 83 | } 84 | 85 | private void addServer(SshServerOption option) { 86 | if (option == null) { 87 | return; 88 | } 89 | int port = 22; 90 | try { 91 | port = Integer.parseInt(option.getPort().toString()) & 0xffff; 92 | } catch (Exception e) { 93 | } 94 | SshServer sshServer; 95 | if (TextUtils.isEmpty(option.getId())) { 96 | sshServer = new SshServer( 97 | option.getHost().toString(), 98 | port, 99 | option.getUsername().toString(), 100 | option.getPassword().toString(), 101 | option.getPrivateKey().toString(), 102 | option.getPassphrase().toString()); 103 | } else { 104 | sshServer = new SshServer( 105 | option.getId(), 106 | option.getHost().toString(), 107 | port, 108 | option.getUsername().toString(), 109 | option.getPassword().toString(), 110 | option.getPrivateKey().toString(), 111 | option.getPassphrase().toString(), 112 | true, 113 | option.getOrder()); 114 | } 115 | mViewModel.addServer(sshServer); 116 | } 117 | 118 | @Subscribe 119 | public void onItemClick(TerminalItem.ItemClickEvent event) { 120 | dismiss(); 121 | } 122 | 123 | @Subscribe 124 | public void onItemLongClick(TerminalItem.ItemLongClickEvent event) { 125 | if (event.item.getSshServer() == null) { 126 | return; 127 | } 128 | 129 | SshServerOption option = new SshServerOption(event.item.getSshServer()); 130 | CreateSshServerDialogFragment fragment = CreateSshServerDialogFragment.newInstance(R.string.edit_ssh_server, option); 131 | fragment.setResultCallback(this::addServer); 132 | 133 | fragment.show(getParentFragmentManager(), null); 134 | } 135 | 136 | 137 | @Override 138 | public void onDestroy() { 139 | super.onDestroy(); 140 | EventBus.getDefault().unregister(this); 141 | } 142 | 143 | static class ServerItemAdapter extends RecyclerView.Adapter { 144 | 145 | private final TerminalManagerViewModel mViewModel; 146 | 147 | private List mItemList; 148 | 149 | private RecyclerViewAdapterChangedCallback mListChangedCallback; 150 | 151 | ServerItemAdapter(@NonNull TerminalManagerViewModel viewModel) { 152 | this.mViewModel = viewModel; 153 | setList(mViewModel.terminals); 154 | } 155 | 156 | public void setList(@NonNull List items) { 157 | if (this.mItemList == items) { 158 | return; 159 | } 160 | if (this.mItemList instanceof ObservableList) { 161 | //noinspection unchecked 162 | ((ObservableList) this.mItemList).removeOnListChangedCallback(mListChangedCallback); 163 | } 164 | this.mItemList = items; 165 | if (this.mItemList instanceof ObservableList) { 166 | if (mListChangedCallback == null) { 167 | mListChangedCallback = new RecyclerViewAdapterChangedCallback(this); 168 | } 169 | //noinspection unchecked 170 | ((ObservableList) this.mItemList).addOnListChangedCallback(mListChangedCallback); 171 | } 172 | notifyDataSetChanged(); 173 | } 174 | 175 | @NonNull 176 | @Override 177 | public ListItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 178 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 179 | ListItemBinding binding = DataBindingUtil.inflate(inflater, R.layout.list_item, parent, false); 180 | return new ListItemViewHolder(binding); 181 | } 182 | 183 | @Override 184 | public void onBindViewHolder(@NonNull ListItemViewHolder holder, int position) { 185 | int layoutPosition = holder.getLayoutPosition(); 186 | TerminalItem item = mItemList.get(layoutPosition); 187 | ListItemBinding binding = holder.binding; 188 | 189 | binding.setViewModel(mViewModel); 190 | binding.setItem(item); 191 | 192 | binding.executePendingBindings(); 193 | 194 | } 195 | 196 | @Override 197 | public int getItemCount() { 198 | return mItemList.size(); 199 | } 200 | 201 | static class ListItemViewHolder extends RecyclerView.ViewHolder { 202 | private final ListItemBinding binding; 203 | 204 | ListItemViewHolder(@NonNull ListItemBinding binding) { 205 | super(binding.getRoot()); 206 | this.binding = binding; 207 | } 208 | } 209 | 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/db/ATermDatabase.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.db; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.room.Database; 6 | import androidx.room.Room; 7 | import androidx.room.RoomDatabase; 8 | 9 | import com.github.maoabc.aterm.db.entities.SshServer; 10 | import com.github.maoabc.aterm.db.source.SshServerDao; 11 | 12 | 13 | /** 14 | * Created by mao on 17-11-19. 15 | */ 16 | 17 | @Database(entities = 18 | {SshServer.class}, 19 | version = 1, exportSchema = false) 20 | public abstract class ATermDatabase extends RoomDatabase { 21 | private volatile static ATermDatabase sInstance; 22 | 23 | private static final String DATABASE_NAME = "sshServers.db"; 24 | 25 | 26 | public abstract SshServerDao sshServerDao(); 27 | 28 | 29 | public static ATermDatabase getInstance(final Context context) { 30 | if (sInstance == null) { 31 | synchronized (ATermDatabase.class) { 32 | if (sInstance == null) { 33 | sInstance = buildDatabase(context.getApplicationContext()); 34 | } 35 | } 36 | } 37 | return sInstance; 38 | } 39 | 40 | /** 41 | * Build the database. {@link Builder#build()} only sets up the database configuration and 42 | * creates a new instance of the database. 43 | * The SQLite database is only created when it's accessed for the first time. 44 | */ 45 | private static ATermDatabase buildDatabase(final Context appContext) { 46 | return Room.databaseBuilder(appContext, ATermDatabase.class, DATABASE_NAME).build(); 47 | } 48 | 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/db/entities/SshServer.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.db.entities; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.room.ColumnInfo; 5 | import androidx.room.Entity; 6 | import androidx.room.Ignore; 7 | import androidx.room.PrimaryKey; 8 | 9 | import java.util.UUID; 10 | 11 | 12 | @Entity(tableName = "ssh_servers") 13 | public class SshServer { 14 | @NonNull 15 | @PrimaryKey 16 | @ColumnInfo(name = "_id") 17 | private String id; 18 | 19 | @NonNull 20 | @ColumnInfo(name = "host") 21 | private String host; 22 | 23 | @ColumnInfo(name = "port") 24 | private int port; 25 | 26 | @NonNull 27 | @ColumnInfo(name = "user") 28 | private String username; 29 | 30 | @ColumnInfo(name = "password") 31 | private String password; 32 | 33 | @ColumnInfo(name = "private_key") 34 | private String privateKey; 35 | 36 | @ColumnInfo(name = "private_key_phase") 37 | private String privateKeyPhase; 38 | 39 | @ColumnInfo 40 | private int compression; 41 | 42 | 43 | @ColumnInfo(name = "order_index") 44 | private int order; 45 | 46 | public SshServer() { 47 | this(UUID.randomUUID().toString(), 22, "", "", "", ""); 48 | } 49 | 50 | @Ignore 51 | public SshServer(@NonNull String host, int port, @NonNull String username, String password, String privateKey, String privateKeyPhase) { 52 | this(UUID.randomUUID().toString(), host, port, username, password, privateKey, privateKeyPhase, true, 0); 53 | } 54 | 55 | @Ignore 56 | public SshServer(@NonNull String id, @NonNull String host, int port, @NonNull String username, String password, String privateKey, String privateKeyPhase, boolean compress, int order) { 57 | this.id = id; 58 | this.host = host; 59 | this.port = port; 60 | this.username = username; 61 | this.password = password; 62 | this.privateKey = privateKey; 63 | this.privateKeyPhase = privateKeyPhase; 64 | this.compression = compress ? 1 : 0; 65 | this.order = order; 66 | } 67 | 68 | @NonNull 69 | public String getId() { 70 | return id; 71 | } 72 | 73 | public void setId(@NonNull String id) { 74 | this.id = id; 75 | } 76 | 77 | @NonNull 78 | public String getHost() { 79 | return host; 80 | } 81 | 82 | public void setHost(@NonNull String host) { 83 | this.host = host; 84 | } 85 | 86 | public int getPort() { 87 | return port; 88 | } 89 | 90 | public void setPort(int port) { 91 | this.port = port; 92 | } 93 | 94 | @NonNull 95 | public String getUsername() { 96 | return username; 97 | } 98 | 99 | public void setUsername(@NonNull String username) { 100 | this.username = username; 101 | } 102 | 103 | public String getPassword() { 104 | return password; 105 | } 106 | 107 | public void setPassword(String password) { 108 | this.password = password; 109 | } 110 | 111 | public String getPrivateKey() { 112 | return privateKey; 113 | } 114 | 115 | public void setPrivateKey(String privateKey) { 116 | this.privateKey = privateKey; 117 | } 118 | 119 | public String getPrivateKeyPhase() { 120 | return privateKeyPhase; 121 | } 122 | 123 | public void setPrivateKeyPhase(String privateKeyPhase) { 124 | this.privateKeyPhase = privateKeyPhase; 125 | } 126 | 127 | public int getCompression() { 128 | return compression; 129 | } 130 | 131 | public void setCompression(int compression) { 132 | this.compression = compression; 133 | } 134 | 135 | public int getOrder() { 136 | return order; 137 | } 138 | 139 | public void setOrder(int order) { 140 | this.order = order; 141 | } 142 | 143 | @Override 144 | public boolean equals(Object o) { 145 | if (this == o) return true; 146 | if (o == null || getClass() != o.getClass()) return false; 147 | 148 | SshServer sshServer = (SshServer) o; 149 | 150 | return id.equals(sshServer.id); 151 | } 152 | 153 | @Override 154 | public int hashCode() { 155 | return id.hashCode(); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/db/source/SshServerDao.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.db.source; 2 | 3 | import androidx.room.Dao; 4 | import androidx.room.Insert; 5 | import androidx.room.OnConflictStrategy; 6 | import androidx.room.Query; 7 | 8 | import com.github.maoabc.aterm.db.entities.SshServer; 9 | 10 | import java.util.List; 11 | 12 | 13 | /** 14 | * Created by mao on 18-3-7. 15 | */ 16 | 17 | @Dao 18 | public interface SshServerDao { 19 | @Query("select * from ssh_servers order by order_index asc") 20 | List getSshServers(); 21 | 22 | @Query("select * from ssh_servers where _id = :id") 23 | SshServer getSshServer(String id); 24 | 25 | @Insert(onConflict = OnConflictStrategy.REPLACE) 26 | void insertSshServer(SshServer sshServer); 27 | 28 | @Insert(onConflict = OnConflictStrategy.REPLACE) 29 | void insertSshServers(List sshServers); 30 | 31 | 32 | @Query("delete from ssh_servers where _id = :id") 33 | void deleteById(String id); 34 | 35 | @Query("delete from ssh_servers") 36 | void deleteAll(); 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/db/source/SshServerDataSource.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.db.source; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.lifecycle.LiveData; 5 | import androidx.lifecycle.MutableLiveData; 6 | 7 | import com.github.maoabc.aterm.db.entities.SshServer; 8 | import com.github.maoabc.util.AppExecutors; 9 | 10 | import java.util.List; 11 | 12 | 13 | public class SshServerDataSource { 14 | 15 | private final SshServerDao mDeviceDao; 16 | private final AppExecutors mExecutors; 17 | 18 | public SshServerDataSource(SshServerDao sshServerDao, AppExecutors executors) { 19 | this.mDeviceDao = sshServerDao; 20 | this.mExecutors = executors; 21 | } 22 | 23 | public LiveData> getSshServers() { 24 | final MutableLiveData> liveData = new MutableLiveData<>(); 25 | mExecutors.diskIO().execute(() -> { 26 | List remoteDevices = mDeviceDao.getSshServers(); 27 | liveData.postValue(remoteDevices); 28 | }); 29 | return liveData; 30 | } 31 | 32 | public LiveData addSshServer(SshServer sshServer) { 33 | final MutableLiveData liveData = new MutableLiveData<>(); 34 | mExecutors.diskIO().execute(() -> { 35 | mDeviceDao.insertSshServer(sshServer); 36 | liveData.postValue(true); 37 | }); 38 | return liveData; 39 | } 40 | 41 | public LiveData addSshServers(List sshServers) { 42 | final MutableLiveData liveData = new MutableLiveData<>(); 43 | mExecutors.diskIO().execute(() -> { 44 | mDeviceDao.insertSshServers(sshServers); 45 | liveData.postValue(true); 46 | }); 47 | return liveData; 48 | } 49 | 50 | public LiveData delete(@NonNull SshServer sshServer) { 51 | final MutableLiveData liveData = new MutableLiveData<>(); 52 | mExecutors.diskIO().execute(() -> { 53 | mDeviceDao.deleteById(sshServer.getId()); 54 | liveData.postValue(true); 55 | }); 56 | return liveData; 57 | } 58 | 59 | public void deleteAll() { 60 | mExecutors.diskIO().execute(mDeviceDao::deleteAll); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ssh/ByteQueue.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.ssh; 2 | 3 | 4 | class ByteQueue { 5 | ByteQueue(int size) { 6 | mBuffer = new byte[size]; 7 | } 8 | 9 | int getBytesAvailable() { 10 | synchronized (this) { 11 | return mStoredBytes; 12 | } 13 | } 14 | 15 | int read(byte[] buffer, int offset, int length) 16 | throws InterruptedException { 17 | if (length + offset > buffer.length) { 18 | throw 19 | new IllegalArgumentException("length + offset > buffer.length"); 20 | } 21 | if (length < 0) { 22 | throw 23 | new IllegalArgumentException("length < 0"); 24 | 25 | } 26 | if (length == 0) { 27 | return 0; 28 | } 29 | synchronized (this) { 30 | while (mStoredBytes == 0) { 31 | wait(); 32 | } 33 | int totalRead = 0; 34 | int bufferLength = mBuffer.length; 35 | boolean wasFull = bufferLength == mStoredBytes; 36 | while (length > 0 && mStoredBytes > 0) { 37 | int oneRun = Math.min(bufferLength - mHead, mStoredBytes); 38 | int bytesToCopy = Math.min(length, oneRun); 39 | System.arraycopy(mBuffer, mHead, buffer, offset, bytesToCopy); 40 | mHead += bytesToCopy; 41 | if (mHead >= bufferLength) { 42 | mHead = 0; 43 | } 44 | mStoredBytes -= bytesToCopy; 45 | length -= bytesToCopy; 46 | offset += bytesToCopy; 47 | totalRead += bytesToCopy; 48 | } 49 | if (wasFull) { 50 | notify(); 51 | } 52 | return totalRead; 53 | } 54 | } 55 | 56 | /** 57 | * Attempt to write the specified portion of the provided buffer to 58 | * the queue. Returns the number of bytes actually written to the queue; 59 | * it is the caller's responsibility to check whether all of the data 60 | * was written and repeat the call to write() if necessary. 61 | */ 62 | int write(byte[] buffer, int offset, int length) 63 | throws InterruptedException { 64 | if (length + offset > buffer.length) { 65 | throw 66 | new IllegalArgumentException("length + offset > buffer.length"); 67 | } 68 | if (length < 0) { 69 | throw 70 | new IllegalArgumentException("length < 0"); 71 | 72 | } 73 | if (length == 0) { 74 | return 0; 75 | } 76 | synchronized (this) { 77 | int bufferLength = mBuffer.length; 78 | boolean wasEmpty = mStoredBytes == 0; 79 | while (bufferLength == mStoredBytes) { 80 | wait(); 81 | } 82 | int tail = mHead + mStoredBytes; 83 | int oneRun; 84 | if (tail >= bufferLength) { 85 | tail = tail - bufferLength; 86 | oneRun = mHead - tail; 87 | } else { 88 | oneRun = bufferLength - tail; 89 | } 90 | int bytesToCopy = Math.min(oneRun, length); 91 | System.arraycopy(buffer, offset, mBuffer, tail, bytesToCopy); 92 | offset += bytesToCopy; 93 | mStoredBytes += bytesToCopy; 94 | if (wasEmpty) { 95 | notify(); 96 | } 97 | return bytesToCopy; 98 | } 99 | } 100 | 101 | private byte[] mBuffer; 102 | private int mHead; 103 | private int mStoredBytes; 104 | } 105 | 106 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/ssh/SshTerminal.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.ssh; 2 | 3 | import android.os.Handler; 4 | import android.os.HandlerThread; 5 | import android.os.Message; 6 | import android.text.TextUtils; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | 11 | import com.github.maoabc.aterm.ATermSettings; 12 | import com.jcraft.jsch.ChannelShell; 13 | import com.jcraft.jsch.JSch; 14 | import com.jcraft.jsch.JSchException; 15 | import com.jcraft.jsch.Session; 16 | 17 | import java.io.IOException; 18 | import java.io.OutputStream; 19 | 20 | import aterm.terminal.AbstractTerminal; 21 | 22 | public class SshTerminal extends AbstractTerminal { 23 | private static final int MSG_NOTIFY_PTY_WRITE = 1; 24 | private static final int MSG_NOTIFY_PTY_FLUSH = 2; 25 | private static final int MSG_NOTIFY_PTY_RESIZE = 3; 26 | public static final String TAG = "SSH"; 27 | 28 | private final ByteQueue mByteQueue = new ByteQueue(4096); 29 | 30 | private final String mKey; 31 | private String mTitle; 32 | private volatile Session mSession; 33 | private volatile ChannelShell mShell; 34 | private Handler mWriterHandler; 35 | private int mRows; 36 | private int mCols; 37 | 38 | @NonNull 39 | private String mHost; 40 | 41 | private int mPort = 22; 42 | 43 | private String mUsername; 44 | 45 | private String mPassword; 46 | 47 | private String mPrivateKey = ""; 48 | private String mPassphrase = ""; 49 | private HandlerThread mHandlerThread; 50 | 51 | public SshTerminal(ATermSettings settings, @NonNull String host, int port, 52 | @NonNull String username, String password, 53 | String privateKey, String passphrase, 54 | @NonNull String key) { 55 | super(50, 30, 100, settings.getColorScheme()[0], settings.getColorScheme()[1]); 56 | mRows = 50; 57 | mCols = 30; 58 | this.mHost = host; 59 | this.mPort = port; 60 | this.mUsername = username; 61 | this.mPassword = password; 62 | this.mPrivateKey = privateKey; 63 | this.mPassphrase = passphrase; 64 | this.mKey = key; 65 | this.mTitle = key; 66 | } 67 | 68 | 69 | @Override 70 | public void start() { 71 | 72 | 73 | new Thread("Pty reader") { 74 | @Override 75 | public void run() { 76 | try { 77 | JSch jSch = new JSch(); 78 | if (!TextUtils.isEmpty(mPrivateKey)) { 79 | try { 80 | jSch.addIdentity(mUsername + "@" + mHost, 81 | mPrivateKey.getBytes(), null, 82 | mPassphrase == null ? null : mPassphrase.getBytes()); 83 | } catch (JSchException e) { 84 | Log.e(TAG, "addIdentity: ", e); 85 | } 86 | 87 | } 88 | 89 | mSession = jSch.getSession(mUsername, mHost, mPort); 90 | mSession.setConfig("PreferredAuthentications", "publickey,password"); 91 | //todo 检查服务器key 92 | mSession.setConfig("StrictHostKeyChecking", "no"); 93 | 94 | mSession.setConfig("ConnectTimeout", "30"); 95 | 96 | mSession.setConfig("compression.s2c", "zlib@openssh.com,zlib,none"); 97 | mSession.setConfig("compression.c2s", "zlib@openssh.com,zlib,none"); 98 | mSession.setConfig("compression_level", "-1"); 99 | 100 | if (TextUtils.isEmpty(mPrivateKey) && !TextUtils.isEmpty(mPassword)) { 101 | mSession.setPassword(mPassword.getBytes()); 102 | } 103 | 104 | mSession.connect(30000); 105 | 106 | mShell = (ChannelShell) mSession.openChannel("shell"); 107 | mShell.setPtyType("xterm-256color"); 108 | mShell.connect(); 109 | mShell.setOutputStream(new OutputStream() { 110 | byte[] buf = new byte[1]; 111 | 112 | @Override 113 | public void write(int b) throws IOException { 114 | buf[0] = (byte) b; 115 | write(buf, 0, 1); 116 | } 117 | 118 | @Override 119 | public void write(byte[] b) throws IOException { 120 | write(b, 0, b.length); 121 | } 122 | 123 | @Override 124 | public void write(byte[] b, int off, int len) throws IOException { 125 | inputWrite(b, off, len); 126 | } 127 | 128 | @Override 129 | public void close() throws IOException { 130 | if (mDestroyCallback != null) { 131 | mDestroyCallback.onDestroy(SshTerminal.this, mShell.getExitStatus()); 132 | } 133 | } 134 | }); 135 | mShell.setPtySize(mCols, mCols, 0, 0); 136 | } catch (Exception e) { 137 | Log.e(TAG, "Pty reader: ", e); 138 | if (mDestroyCallback != null) { 139 | mDestroyCallback.onDestroy(SshTerminal.this, -1); 140 | } 141 | } 142 | } 143 | }.start(); 144 | //写入数据到pty,消息循环 145 | mHandlerThread = new HandlerThread("Pty writer"); 146 | mHandlerThread.start(); 147 | mWriterHandler = new Handler(mHandlerThread.getLooper()) { 148 | OutputStream outputStream; 149 | 150 | @Override 151 | public void handleMessage(@NonNull Message msg) { 152 | switch (msg.what) { 153 | case MSG_NOTIFY_PTY_WRITE: { 154 | byte[] bytes = new byte[4096]; 155 | try { 156 | int read = mByteQueue.read(bytes, 0, Math.min(mByteQueue.getBytesAvailable(), bytes.length)); 157 | if (read > 0) { 158 | if (outputStream == null) { 159 | if (mShell == null) { 160 | Log.e(TAG, "Not start shell"); 161 | return; 162 | } 163 | outputStream = mShell.getOutputStream(); 164 | } 165 | outputStream.write(bytes, 0, read); 166 | 167 | } 168 | } catch (IOException | InterruptedException e) { 169 | Log.e(TAG, "writeToPty: ", e); 170 | } 171 | break; 172 | } 173 | case MSG_NOTIFY_PTY_RESIZE: { 174 | if (mShell != null) mShell.setPtySize(msg.arg1, msg.arg2, 0, 0); 175 | break; 176 | } 177 | case MSG_NOTIFY_PTY_FLUSH: { 178 | try { 179 | if (outputStream != null) outputStream.flush(); 180 | } catch (IOException e) { 181 | } 182 | } 183 | } 184 | } 185 | }; 186 | 187 | } 188 | 189 | @NonNull 190 | @Override 191 | public String getTitle() { 192 | return mTitle; 193 | } 194 | 195 | @Override 196 | public void setTitle(@NonNull String title) { 197 | this.mTitle = title; 198 | } 199 | 200 | @NonNull 201 | @Override 202 | public String getKey() { 203 | return mKey; 204 | } 205 | 206 | @Override 207 | protected void setPtyWindowSize(int cols, int rows) { 208 | if (mWriterHandler == null) { 209 | Log.e(TAG, "setPtyWindowSize: Handler null"); 210 | return; 211 | } 212 | mCols = cols; 213 | mRows = rows; 214 | Message message = mWriterHandler.obtainMessage(MSG_NOTIFY_PTY_RESIZE, cols, rows); 215 | mWriterHandler.sendMessage(message); 216 | } 217 | 218 | @Override 219 | protected void closePty() { 220 | if (mShell != null) { 221 | mShell.disconnect(); 222 | } 223 | if (mSession != null) { 224 | mSession.disconnect(); 225 | } 226 | if (mHandlerThread != null) mHandlerThread.quitSafely(); 227 | } 228 | 229 | 230 | @Override 231 | protected int scrollRowSize() { 232 | //todo 233 | return 1000; 234 | } 235 | 236 | @Override 237 | public void flushToPty() { 238 | if (mWriterHandler == null) { 239 | Log.e(TAG, "flushToPty: "); 240 | return; 241 | } 242 | mWriterHandler.sendEmptyMessage(MSG_NOTIFY_PTY_FLUSH); 243 | } 244 | 245 | @Override 246 | public void release() { 247 | 248 | } 249 | 250 | 251 | @Override 252 | public void writeToPty(byte[] bytes, int len) { 253 | if (mWriterHandler == null) { 254 | return; 255 | } 256 | try { 257 | mByteQueue.write(bytes, 0, len); 258 | mWriterHandler.sendEmptyMessage(MSG_NOTIFY_PTY_WRITE); 259 | } catch (InterruptedException e) { 260 | Log.e(TAG, "writeToPty", e); 261 | } 262 | 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/viewmodel/SshServerOption.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.viewmodel; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.text.TextUtils; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.databinding.BaseObservable; 9 | import androidx.databinding.Bindable; 10 | 11 | import com.github.maoabc.aterm.BR; 12 | import com.github.maoabc.aterm.db.entities.SshServer; 13 | 14 | 15 | public class SshServerOption extends BaseObservable implements Parcelable { 16 | @NonNull 17 | private final String id; 18 | 19 | @NonNull 20 | private String host = ""; 21 | 22 | @NonNull 23 | private String port = "22"; 24 | 25 | @NonNull 26 | private String username = ""; 27 | @NonNull 28 | private String password = ""; 29 | 30 | private boolean compression; 31 | 32 | 33 | private boolean enableKey = false; 34 | 35 | @NonNull 36 | private String privateKey = ""; 37 | 38 | @NonNull 39 | private String passphrase = ""; 40 | 41 | private int order = -1; 42 | 43 | public SshServerOption() { 44 | id = ""; 45 | } 46 | 47 | public SshServerOption(SshServer server) { 48 | this.id = server.getId(); 49 | 50 | this.host = server.getHost(); 51 | 52 | this.port = server.getPort() + ""; 53 | 54 | this.username = server.getUsername(); 55 | 56 | this.password = server.getPassword(); 57 | 58 | this.compression = true; 59 | 60 | this.privateKey = server.getPrivateKey(); 61 | 62 | this.enableKey = !TextUtils.isEmpty(this.privateKey); 63 | 64 | this.passphrase = server.getPrivateKeyPhase(); 65 | 66 | this.order = server.getOrder(); 67 | 68 | } 69 | 70 | @NonNull 71 | @Bindable 72 | public CharSequence getHost() { 73 | return host; 74 | } 75 | 76 | public void setHost(CharSequence host) { 77 | if (!host.equals(this.host)) { 78 | this.host = host.toString(); 79 | notifyPropertyChanged(BR.host); 80 | } 81 | } 82 | 83 | @NonNull 84 | @Bindable 85 | public CharSequence getPort() { 86 | return port; 87 | } 88 | 89 | public void setPort(@NonNull CharSequence port) { 90 | if (!port.equals(this.port)) { 91 | this.port = port.toString(); 92 | notifyPropertyChanged(BR.port); 93 | } 94 | } 95 | 96 | @NonNull 97 | @Bindable 98 | public CharSequence getUsername() { 99 | return username; 100 | } 101 | 102 | public void setUsername(CharSequence username) { 103 | if (!username.equals(this.username)) { 104 | this.username = username.toString(); 105 | notifyPropertyChanged(BR.username); 106 | } 107 | } 108 | 109 | @NonNull 110 | @Bindable 111 | public CharSequence getPassword() { 112 | return password; 113 | } 114 | 115 | public void setPassword(CharSequence password) { 116 | if (!password.equals(this.password)) { 117 | this.password = password.toString(); 118 | notifyPropertyChanged(BR.password); 119 | } 120 | } 121 | 122 | @Bindable 123 | public boolean isCompression() { 124 | return compression; 125 | } 126 | 127 | public void setCompression(boolean compression) { 128 | if (this.compression != compression) { 129 | this.compression = compression; 130 | notifyPropertyChanged(BR.compression); 131 | } 132 | } 133 | 134 | 135 | @Bindable 136 | public boolean isEnableKey() { 137 | return enableKey; 138 | } 139 | 140 | public void setEnableKey(boolean enableKey) { 141 | if (enableKey != this.enableKey) { 142 | this.enableKey = enableKey; 143 | notifyPropertyChanged(BR.enableKey); 144 | } 145 | } 146 | 147 | @NonNull 148 | @Bindable 149 | public CharSequence getPrivateKey() { 150 | return privateKey; 151 | } 152 | 153 | 154 | public void setPrivateKey(CharSequence privateKey) { 155 | if (!privateKey.equals(this.privateKey)) { 156 | this.privateKey = privateKey.toString(); 157 | notifyPropertyChanged(BR.privateKey); 158 | } 159 | } 160 | 161 | @NonNull 162 | @Bindable 163 | public CharSequence getPassphrase() { 164 | return passphrase; 165 | } 166 | 167 | public void setPassphrase(CharSequence passphrase) { 168 | if (!passphrase.equals(this.passphrase)) { 169 | this.passphrase = passphrase.toString(); 170 | notifyPropertyChanged(BR.passphrase); 171 | } 172 | } 173 | 174 | @NonNull 175 | public String getId() { 176 | return id; 177 | } 178 | 179 | public int getOrder() { 180 | return order; 181 | } 182 | 183 | @Override 184 | public int describeContents() { 185 | return 0; 186 | } 187 | 188 | @Override 189 | public void writeToParcel(Parcel dest, int flags) { 190 | dest.writeString(this.id); 191 | dest.writeString(this.host); 192 | dest.writeString(this.port); 193 | dest.writeString(this.username); 194 | dest.writeString(this.password); 195 | dest.writeByte(this.compression ? (byte) 1 : (byte) 0); 196 | dest.writeByte(this.enableKey ? (byte) 1 : (byte) 0); 197 | dest.writeString(this.privateKey); 198 | dest.writeString(this.passphrase); 199 | } 200 | 201 | protected SshServerOption(Parcel in) { 202 | this.id = in.readString(); 203 | this.host = in.readString(); 204 | this.port = in.readString(); 205 | this.username = in.readString(); 206 | this.password = in.readString(); 207 | this.compression = in.readByte() != 0; 208 | this.enableKey = in.readByte() != 0; 209 | this.privateKey = in.readString(); 210 | this.passphrase = in.readString(); 211 | } 212 | 213 | public static final Creator CREATOR = new Creator() { 214 | @Override 215 | public SshServerOption createFromParcel(Parcel source) { 216 | return new SshServerOption(source); 217 | } 218 | 219 | @Override 220 | public SshServerOption[] newArray(int size) { 221 | return new SshServerOption[size]; 222 | } 223 | }; 224 | } 225 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/viewmodel/TerminalItem.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.viewmodel; 2 | 3 | import android.view.View; 4 | 5 | import com.github.maoabc.aterm.db.entities.SshServer; 6 | 7 | import org.greenrobot.eventbus.EventBus; 8 | 9 | 10 | public class TerminalItem { 11 | private final SshServer sshServer; 12 | 13 | TerminalItem(SshServer sshServer) { 14 | this.sshServer = sshServer; 15 | } 16 | 17 | public String getName() { 18 | if (sshServer == null) { 19 | return "Local Terminal"; 20 | } 21 | return sshServer.getUsername() + "@" + sshServer.getHost(); 22 | } 23 | 24 | public SshServer getSshServer() { 25 | return sshServer; 26 | } 27 | 28 | public boolean isLocal() { 29 | return sshServer == null; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object o) { 34 | if (this == o) return true; 35 | if (o == null || getClass() != o.getClass()) return false; 36 | 37 | TerminalItem that = (TerminalItem) o; 38 | 39 | return sshServer != null && sshServer.equals(that.sshServer); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return sshServer != null ? sshServer.hashCode() : super.hashCode(); 45 | } 46 | 47 | public void onClick(View v) { 48 | EventBus.getDefault().post(new ItemClickEvent(this)); 49 | } 50 | 51 | public boolean onLongClick(View v) { 52 | if (sshServer == null) { 53 | return false; 54 | } 55 | EventBus.getDefault().post(new ItemLongClickEvent(this)); 56 | return true; 57 | } 58 | 59 | public static class ItemClickEvent { 60 | public final TerminalItem item; 61 | 62 | public ItemClickEvent(TerminalItem item) { 63 | this.item = item; 64 | } 65 | } 66 | 67 | public static class ItemLongClickEvent { 68 | public final TerminalItem item; 69 | 70 | public ItemLongClickEvent(TerminalItem item) { 71 | this.item = item; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/aterm/viewmodel/TerminalManagerViewModel.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.aterm.viewmodel; 2 | 3 | import androidx.databinding.ObservableArrayList; 4 | import androidx.databinding.ObservableList; 5 | import androidx.lifecycle.ViewModel; 6 | 7 | import com.github.maoabc.BaseApp; 8 | import com.github.maoabc.aterm.db.ATermDatabase; 9 | import com.github.maoabc.aterm.db.entities.SshServer; 10 | import com.github.maoabc.aterm.db.source.SshServerDataSource; 11 | 12 | 13 | public class TerminalManagerViewModel extends ViewModel { 14 | public final ObservableList terminals = new ObservableArrayList<>(); 15 | private SshServerDataSource sshServerDataSource; 16 | 17 | public TerminalManagerViewModel() { 18 | loadTerminals(); 19 | } 20 | 21 | private void loadTerminals() { 22 | terminals.clear(); 23 | terminals.add(new TerminalItem(null));//local terminal 24 | BaseApp context = BaseApp.get(); 25 | sshServerDataSource = new SshServerDataSource(ATermDatabase.getInstance(context).sshServerDao(), BaseApp.get().getAppExecutors()); 26 | sshServerDataSource.getSshServers() 27 | .observeForever(sshServers -> { 28 | if (sshServers == null) { 29 | return; 30 | } 31 | for (SshServer server : sshServers) { 32 | terminals.add(new TerminalItem(server)); 33 | } 34 | }); 35 | } 36 | 37 | public void addServer(SshServer sshServer) { 38 | if (sshServer.getOrder() == -1) { 39 | sshServer.setOrder(terminals.size()); 40 | } 41 | sshServerDataSource 42 | .addSshServer(sshServer) 43 | .observeForever(b -> { 44 | if (!b) { 45 | return; 46 | } 47 | loadTerminals(); 48 | }); 49 | } 50 | 51 | public void deleteServer(final TerminalItem item) { 52 | if (item.getSshServer() == null) { 53 | return; 54 | } 55 | sshServerDataSource.delete(item.getSshServer()).observeForever(b -> { 56 | if (!b) { 57 | return; 58 | } 59 | terminals.remove(item); 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/fragment/RetainedDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.common.fragment; 2 | 3 | import android.os.Bundle; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.fragment.app.DialogFragment; 8 | import androidx.fragment.app.FragmentManager; 9 | import androidx.fragment.app.FragmentTransaction; 10 | 11 | public class RetainedDialogFragment extends DialogFragment { 12 | 13 | @Override 14 | public void onCreate(@Nullable Bundle savedInstanceState) { 15 | super.onCreate(savedInstanceState); 16 | setRetainInstance(true); 17 | } 18 | 19 | @Override 20 | public void onDestroyView() { 21 | if (getDialog() != null && getRetainInstance()) { 22 | getDialog().setDismissMessage(null); 23 | } 24 | super.onDestroyView(); 25 | } 26 | 27 | @Override 28 | public void show(@NonNull FragmentManager manager, String tag) { 29 | try { 30 | super.show(manager, tag); 31 | } catch (Exception e) { 32 | FragmentTransaction transaction = manager.beginTransaction(); 33 | transaction.add(this, tag); 34 | transaction.commitAllowingStateLoss(); 35 | } 36 | } 37 | 38 | public interface ResultCallback { 39 | void onResult(String text); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/fragment/TextFieldDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.common.fragment; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.app.Dialog; 6 | import android.content.Context; 7 | import android.content.DialogInterface; 8 | import android.os.Bundle; 9 | import android.os.Handler; 10 | import android.os.Looper; 11 | import android.text.InputType; 12 | import android.text.TextUtils; 13 | import android.view.LayoutInflater; 14 | import android.view.View; 15 | import android.view.inputmethod.InputMethodManager; 16 | import android.widget.Button; 17 | import android.widget.EditText; 18 | 19 | import androidx.annotation.NonNull; 20 | import androidx.appcompat.app.AlertDialog; 21 | import androidx.fragment.app.FragmentActivity; 22 | 23 | import com.github.maoabc.BaseApp; 24 | import com.github.maoabc.aterm.R; 25 | import com.github.maoabc.util.Precondition; 26 | import com.google.android.material.textfield.TextInputLayout; 27 | 28 | 29 | /** 30 | * 通用文本输入,用来新建或者重命名文件等 31 | * Created by mao on 17-8-15. 32 | */ 33 | 34 | public class TextFieldDialogFragment extends RetainedDialogFragment { 35 | private static final String TITLE_KEY = "title_key"; 36 | private static final String HINT_KEY = "hint_key"; 37 | private static final String TEXT_KEY = "text_key"; 38 | private static final String SELECT_ALL_KEY = "select_all_key"; 39 | private static final String INPUT_TYPE_KEY = "input_type_key"; 40 | 41 | 42 | private String mTitle; 43 | 44 | private String mHint; 45 | private String mText; 46 | private int mInputType; 47 | 48 | private boolean mSelectAll; 49 | 50 | private EditText mInputText; 51 | private TextInputLayout inputLayout; 52 | 53 | private ResultCallback mResultCallback; 54 | 55 | private final Handler mHandler = new Handler(Looper.getMainLooper()); 56 | 57 | public static TextFieldDialogFragment newInstance(String title, String hint, String initText) { 58 | return newInstance(title, hint, initText, false, InputType.TYPE_CLASS_TEXT); 59 | } 60 | 61 | public static TextFieldDialogFragment newInstance(String title, String hint, String initText, boolean selectAll) { 62 | return newInstance(title, hint, initText, selectAll, InputType.TYPE_CLASS_TEXT); 63 | } 64 | 65 | public static TextFieldDialogFragment newInstance(String title, String hint, String initText, boolean selectAll, int inputType) { 66 | 67 | Bundle args = new Bundle(); 68 | 69 | TextFieldDialogFragment fragment = new TextFieldDialogFragment(); 70 | args.putString(TITLE_KEY, title); 71 | args.putString(HINT_KEY, hint); 72 | args.putString(TEXT_KEY, initText); 73 | args.putBoolean(SELECT_ALL_KEY, selectAll); 74 | args.putInt(INPUT_TYPE_KEY, inputType); 75 | 76 | fragment.setArguments(args); 77 | return fragment; 78 | } 79 | 80 | @Override 81 | public void onAttach(@NonNull Context context) { 82 | super.onAttach(context); 83 | 84 | Bundle args = getArguments(); 85 | Precondition.checkNotNull(args); 86 | 87 | mTitle = args.getString(TITLE_KEY, ""); 88 | mHint = args.getString(HINT_KEY, ""); 89 | mText = args.getString(TEXT_KEY, ""); 90 | mSelectAll = args.getBoolean(SELECT_ALL_KEY); 91 | mInputType = args.getInt(INPUT_TYPE_KEY, InputType.TYPE_CLASS_TEXT); 92 | 93 | } 94 | 95 | @NonNull 96 | @Override 97 | public Dialog onCreateDialog(Bundle savedInstanceState) { 98 | 99 | LayoutInflater inflater = LayoutInflater.from(getContext()); 100 | @SuppressLint("InflateParams") View view = inflater.inflate(R.layout.fragment_text_field, null); 101 | 102 | inputLayout = view.findViewById(R.id.input_layout); 103 | mInputText = view.findViewById(R.id.et_input); 104 | 105 | 106 | FragmentActivity context = getActivity(); 107 | Precondition.checkNotNull(context); 108 | 109 | AlertDialog alertDialog = new AlertDialog.Builder(context) 110 | .setTitle(mTitle) 111 | .setView(view) 112 | .setPositiveButton(R.string.ok, null) 113 | .setNegativeButton(R.string.cancel, null) 114 | .create(); 115 | 116 | alertDialog.setOnShowListener(dialog -> { 117 | initEditText(); 118 | AlertDialog ad = (AlertDialog) dialog; 119 | Button button = ad.getButton(DialogInterface.BUTTON_POSITIVE); 120 | button.setOnClickListener(v -> { 121 | CharSequence text = mInputText.getText().toString().trim(); 122 | if (TextUtils.isEmpty(text)) { 123 | BaseApp.toast(R.string.empty_text); 124 | return; 125 | } 126 | if (!text.equals(mText) && mResultCallback != null) { 127 | mResultCallback.onResult(text.toString()); 128 | } 129 | hideSoftInput(); 130 | dismiss(); 131 | }); 132 | }); 133 | return alertDialog; 134 | } 135 | 136 | private void initEditText() { 137 | inputLayout.setHint(mHint); 138 | mInputText.requestFocus(); 139 | if (!TextUtils.isEmpty(mText)) { 140 | mInputText.setText(mText); 141 | if (mSelectAll) { 142 | mInputText.setSelection(0, mText.length()); 143 | } else { 144 | int i = mText.lastIndexOf('.'); 145 | mInputText.setSelection(0, i == -1 ? mText.length() : i); 146 | } 147 | } 148 | mInputText.setInputType(mInputType); 149 | mHandler.postDelayed(() -> { 150 | Context context = getContext(); 151 | if (context != null) { 152 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 153 | if (imm != null) imm.showSoftInput(mInputText, 0); 154 | } 155 | }, 100); 156 | } 157 | 158 | private void hideSoftInput() { 159 | Activity context = getActivity(); 160 | if (context != null) { 161 | InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); 162 | if (imm != null) imm.hideSoftInputFromWindow(mInputText.getWindowToken(), 0); 163 | } 164 | } 165 | 166 | 167 | public void setResultCallback(ResultCallback resultCallback) { 168 | this.mResultCallback = resultCallback; 169 | } 170 | 171 | 172 | } 173 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/widget/CheckableButton.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.github.maoabc.common.widget; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.widget.Checkable; 22 | import android.widget.LinearLayout; 23 | 24 | import androidx.appcompat.widget.AppCompatButton; 25 | 26 | /** 27 | * This is a simple wrapper for {@link LinearLayout} that implements the {@link Checkable} 28 | * interface by keeping an internal 'checked' state flag. 29 | *

30 | * This can be used as the root view for a custom list item layout for 31 | * {@link android.widget.AbsListView} elements with a 32 | * {@link android.widget.AbsListView#setChoiceMode(int) choiceMode} set. 33 | */ 34 | public class CheckableButton extends AppCompatButton implements Checkable { 35 | private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; 36 | 37 | private boolean mChecked = false; 38 | 39 | public CheckableButton(Context context) { 40 | super(context); 41 | } 42 | 43 | public CheckableButton(Context context, AttributeSet attrs) { 44 | super(context, attrs); 45 | } 46 | 47 | public CheckableButton(Context context, AttributeSet attrs, int defStyleAttr) { 48 | super(context, attrs, defStyleAttr); 49 | } 50 | 51 | 52 | public boolean isChecked() { 53 | return mChecked; 54 | } 55 | 56 | public void setChecked(boolean b) { 57 | if (b != mChecked) { 58 | mChecked = b; 59 | refreshDrawableState(); 60 | } 61 | } 62 | 63 | public void toggle() { 64 | setChecked(!mChecked); 65 | } 66 | 67 | @Override 68 | public int[] onCreateDrawableState(int extraSpace) { 69 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 70 | if (drawableState != null && isChecked()) { 71 | mergeDrawableStates(drawableState, CHECKED_STATE_SET); 72 | } 73 | return drawableState; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/widget/CheckableDividerRelativeLayout.java: -------------------------------------------------------------------------------- 1 | 2 | package com.github.maoabc.common.widget; 3 | 4 | import android.content.Context; 5 | import android.util.AttributeSet; 6 | import android.widget.Checkable; 7 | 8 | public class CheckableDividerRelativeLayout extends DividerRelativeLayout implements Checkable { 9 | private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked}; 10 | 11 | private boolean mChecked = false; 12 | 13 | public CheckableDividerRelativeLayout(Context context) { 14 | super(context); 15 | } 16 | 17 | public CheckableDividerRelativeLayout(Context context, AttributeSet attrs) { 18 | super(context, attrs); 19 | } 20 | 21 | public CheckableDividerRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { 22 | super(context, attrs, defStyleAttr); 23 | } 24 | 25 | public boolean isChecked() { 26 | return mChecked; 27 | } 28 | 29 | public void setChecked(boolean b) { 30 | if (b != mChecked) { 31 | mChecked = b; 32 | refreshDrawableState(); 33 | } 34 | } 35 | 36 | 37 | public void toggle() { 38 | setChecked(!mChecked); 39 | } 40 | 41 | @Override 42 | public int[] onCreateDrawableState(int extraSpace) { 43 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 44 | if (drawableState != null && isChecked()) { 45 | mergeDrawableStates(drawableState, CHECKED_STATE_SET); 46 | } 47 | return drawableState; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/widget/DividerRelativeLayout.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.common.widget; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.graphics.Canvas; 6 | import android.graphics.Color; 7 | import android.graphics.Paint; 8 | import android.util.AttributeSet; 9 | import android.widget.RelativeLayout; 10 | 11 | import com.github.maoabc.aterm.R; 12 | 13 | 14 | public class DividerRelativeLayout extends RelativeLayout { 15 | private Paint paint; 16 | private float startX; 17 | 18 | public DividerRelativeLayout(Context context) { 19 | super(context); 20 | init(context, null); 21 | } 22 | 23 | public DividerRelativeLayout(Context context, AttributeSet attrs) { 24 | super(context, attrs); 25 | init(context, attrs); 26 | } 27 | 28 | public DividerRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { 29 | super(context, attrs, defStyleAttr); 30 | init(context, attrs); 31 | } 32 | 33 | private void init(Context context, AttributeSet attrs) { 34 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DividerRelativeLayout); 35 | boolean b = a.getBoolean(R.styleable.DividerRelativeLayout_dividerEnable, false); 36 | if (b) { 37 | paint = new Paint(); 38 | int color = a.getColor(R.styleable.DividerRelativeLayout_dividerColor, Color.LTGRAY); 39 | paint.setColor(color); 40 | startX = a.getDimension(R.styleable.DividerRelativeLayout_dividerStart, 0); 41 | } 42 | 43 | a.recycle(); 44 | } 45 | 46 | @Override 47 | protected void onDraw(Canvas canvas) { 48 | super.onDraw(canvas); 49 | if (paint != null) { 50 | canvas.drawLine(startX, getHeight() - 1, getWidth(), getHeight() - 1, paint); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/widget/FixedLinearLayoutManager.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.common.widget; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.util.Log; 6 | 7 | import androidx.recyclerview.widget.LinearLayoutManager; 8 | import androidx.recyclerview.widget.RecyclerView; 9 | 10 | 11 | public class FixedLinearLayoutManager extends LinearLayoutManager { 12 | public FixedLinearLayoutManager(Context context) { 13 | super(context); 14 | } 15 | 16 | public FixedLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { 17 | super(context, orientation, reverseLayout); 18 | } 19 | 20 | public FixedLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 21 | super(context, attrs, defStyleAttr, defStyleRes); 22 | } 23 | 24 | @Override 25 | public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { 26 | try { 27 | super.onLayoutChildren(recycler, state); 28 | } catch (IndexOutOfBoundsException e) { 29 | Log.e("LinearLayoutManager", "onLayoutChildren: ", e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/common/widget/LongPressRepeatImageView.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.common.widget; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.util.AttributeSet; 6 | import android.view.MotionEvent; 7 | import android.view.ViewConfiguration; 8 | 9 | import androidx.appcompat.widget.AppCompatImageView; 10 | 11 | public class LongPressRepeatImageView extends AppCompatImageView { 12 | 13 | private int[] PRESSED_STATE_SET = {android.R.attr.state_pressed}; 14 | private float mLastX; 15 | private float mLastY; 16 | 17 | public LongPressRepeatImageView(Context context) { 18 | super(context); 19 | } 20 | 21 | public LongPressRepeatImageView(Context context, AttributeSet attrs) { 22 | super(context, attrs); 23 | } 24 | 25 | public LongPressRepeatImageView(Context context, AttributeSet attrs, int defStyleAttr) { 26 | super(context, attrs, defStyleAttr); 27 | } 28 | 29 | private int mPressedCount; 30 | private boolean mTouchDown = false; 31 | 32 | // private final Handler mHandler = new Handler(); 33 | 34 | private final Runnable mClickRunnable = new Runnable() { 35 | @Override 36 | public void run() { 37 | mPressedCount++; 38 | performClick(); 39 | // Log.d("LongPress", "run: " + mPressedCount); 40 | postDelayed(this, ViewConfiguration.getKeyRepeatDelay()); 41 | } 42 | }; 43 | 44 | @SuppressLint("ClickableViewAccessibility") 45 | @Override 46 | public boolean onTouchEvent(MotionEvent event) { 47 | switch (event.getAction()) { 48 | case MotionEvent.ACTION_DOWN: { 49 | mTouchDown = true; 50 | refreshDrawableState(); 51 | 52 | mPressedCount = 0; 53 | mLastX = event.getX(); 54 | mLastY = event.getY(); 55 | 56 | 57 | // mHandler.postDelayed(mClickRunnable, 400); 58 | postDelayed(mClickRunnable, ViewConfiguration.getLongPressTimeout()); 59 | 60 | 61 | break; 62 | } 63 | case MotionEvent.ACTION_UP: 64 | case MotionEvent.ACTION_CANCEL: { 65 | removeCallbacks(mClickRunnable); 66 | mTouchDown = false; 67 | refreshDrawableState(); 68 | if (mPressedCount == 0 && (Math.abs(event.getX() - mLastX) < 20 69 | && Math.abs(event.getY() - mLastY) < 20)) {//没有长按,且手指在很小的范围移动,则当成单击事件 70 | performClick(); 71 | } 72 | 73 | // mHandler.removeCallbacks(mClickRunnable); 74 | } 75 | } 76 | return true; 77 | } 78 | 79 | @Override 80 | public int[] onCreateDrawableState(int extraSpace) { 81 | final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); 82 | if (drawableState != null && mTouchDown) { 83 | mergeDrawableStates(drawableState, PRESSED_STATE_SET); 84 | } 85 | return drawableState; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/util/AppExecutors.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.util; 2 | 3 | /** 4 | * Created by mao on 17-11-19. 5 | */ 6 | 7 | import android.os.Handler; 8 | import android.os.Looper; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import java.util.concurrent.Executor; 13 | import java.util.concurrent.Executors; 14 | 15 | /** 16 | * Global executor pools for the whole application. 17 | *

18 | * Grouping tasks like this avoids the effects of task starvation (e.g. disk reads don't wait behind 19 | * webservice requests). 20 | */ 21 | public class AppExecutors { 22 | 23 | private final Executor mDiskIO; 24 | 25 | private final Executor mNetworkIO; 26 | 27 | private final Executor mMainThread; 28 | 29 | private AppExecutors(Executor diskIO, Executor networkIO, Executor mainThread) { 30 | this.mDiskIO = diskIO; 31 | this.mNetworkIO = networkIO; 32 | this.mMainThread = mainThread; 33 | } 34 | 35 | public AppExecutors() { 36 | this(Executors.newScheduledThreadPool(2), Executors.newFixedThreadPool(2), 37 | new MainThreadExecutor()); 38 | } 39 | 40 | public Executor diskIO() { 41 | return mDiskIO; 42 | } 43 | 44 | public Executor networkIO() { 45 | return mNetworkIO; 46 | } 47 | 48 | public Executor mainThread() { 49 | return mMainThread; 50 | } 51 | 52 | private static class MainThreadExecutor implements Executor { 53 | private Handler mainThreadHandler = new Handler(Looper.getMainLooper()); 54 | 55 | @Override 56 | public void execute(@NonNull Runnable command) { 57 | mainThreadHandler.post(command); 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/util/FileUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.util; 2 | 3 | import android.content.ContentResolver; 4 | import android.database.Cursor; 5 | import android.net.Uri; 6 | import android.provider.MediaStore; 7 | 8 | import com.github.maoabc.BaseApp; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.OutputStream; 14 | 15 | public class FileUtils { 16 | 17 | public static String getFileFromUri(Uri uri) { 18 | 19 | String path; 20 | if (uri == null || (path = uri.getPath()) == null) { 21 | return null; 22 | } 23 | 24 | String scheme = uri.getScheme(); 25 | if (ContentResolver.SCHEME_FILE.equals(scheme)) { 26 | return path; 27 | } 28 | Cursor cursor = null; 29 | try { 30 | if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { 31 | ContentResolver contentResolver = BaseApp.get().getContentResolver(); 32 | String authority = uri.getAuthority(); 33 | if (MediaStore.AUTHORITY.equals(authority) || "downloads".equals(authority)) 34 | cursor = contentResolver.query(uri, new String[]{MediaStore.MediaColumns.DATA}, null, null, null); 35 | if (cursor != null && cursor.moveToFirst() && !cursor.isNull(0)) { 36 | return cursor.getString(0); 37 | } 38 | } 39 | } finally { 40 | if (cursor != null) cursor.close(); 41 | } 42 | if (path.startsWith("/root")) {//adm 43 | path = path.substring(5); 44 | } 45 | 46 | if (new File(path).exists()) { 47 | return path; 48 | } 49 | return ""; 50 | } 51 | 52 | public static void copyStream(InputStream input, OutputStream output) throws IOException { 53 | byte[] buf = new byte[64 * 1024]; 54 | int len; 55 | while ((len = input.read(buf, 0, buf.length)) != -1) { 56 | output.write(buf, 0, len); 57 | } 58 | } 59 | 60 | public static void copyStreamAndClose(InputStream input, OutputStream output) { 61 | try { 62 | copyStream(input, output); 63 | } catch (IOException e) { 64 | 65 | } finally { 66 | try { 67 | input.close(); 68 | } catch (IOException ignored) { 69 | } 70 | try { 71 | output.close(); 72 | } catch (IOException ignored) { 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/util/MimeTypes.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.util; 2 | 3 | import android.webkit.MimeTypeMap; 4 | 5 | import androidx.annotation.IntDef; 6 | import androidx.annotation.NonNull; 7 | 8 | import java.util.HashMap; 9 | import java.util.regex.Pattern; 10 | 11 | public final class MimeTypes { 12 | 13 | public static final String SCHEME_ARCHIVE = "archive"; 14 | 15 | public static final Pattern ARCHIVE_PATTERN = Pattern.compile("^.*\\.(?i)(zip|Z|rpm|deb|cpio|lzma|arj|xar|lzh|zst|cab|z[0-9]+|rar|xz|tar|tgz|tbz2|txz|jar|gz|bz2|7z|7z.001)$"); 16 | public static final Pattern MUSIC_PATTERN = Pattern.compile("^.*\\.(?i)(mp3|wma|wav|aac|ogg|m4a|flac|ra|rm|amr|mid)$"); 17 | // public static final Pattern docP = Pattern.compile("^.*\\.(?i)(doc|docx|rtf|odt)$"); 18 | public static final Pattern MOVIE_PATTERN = Pattern.compile("^.*\\.(?i)(3gp|3gpp|mp4|avi|flv|mkv|m4v|rmvb|mpg|mpeg|wmv|mov|vob|ts|divx|asf|avchd|mts|m2ts|webm|wtv)$"); 19 | public static final Pattern IMAGE_PATTERN = Pattern.compile("^.*\\.(?i)(jpg|jpeg|bmp|png|gif|tiff|arw|dng|raw|rw2|srw|webp)$"); 20 | // public static final Pattern MARKUP_PATTERN = Pattern.compile("^.*\\.(?i)(html|xhtml|xml|css|tex|json)$"); 21 | public static final Pattern SCRIPT_PATTERN = Pattern.compile("^.*\\.(?i)(sh|py|bat|awk|sed|bash|zsh|csh)$"); 22 | // public static final Pattern excelP = Pattern.compile("^.*\\.(?i)(xls|xlsx|ods)$"); 23 | // public static final Pattern pwrpointP = Pattern.compile("^.*\\.(?i)(ppt|pptx|pps|ppsx|odp)$"); 24 | public static final Pattern TEXT_PATTERN = Pattern.compile("^.*\\.(?i)(txt|log|cfg|ini|rc|prop|csv|conf|java|h|hpp|c|cc|cpp|cxx|js|py|lua|diff|md|html|json|lisp|php|rb|rs|pl|go|awk|sed|hs)$"); 25 | public static final Pattern APK_PATTERN = Pattern.compile("^.*\\.(?i)(apk)$"); 26 | 27 | public static final Pattern SHELL_SCRIPT_PATTERN = Pattern.compile("^.*\\.(?i)(sh|bash|zsh)$"); 28 | 29 | @IntDef({OTHER, APK, IMAGE, TEXT, MUSIC, MOVIE, ARCHIVE, SCRIPT}) 30 | @interface MimeType { 31 | } 32 | 33 | public static final int OTHER = 0; 34 | public static final int APK = 1; 35 | public static final int IMAGE = 2; 36 | public static final int TEXT = 3; 37 | public static final int MUSIC = 4; 38 | public static final int MOVIE = 5; 39 | public static final int ARCHIVE = 6; 40 | public static final int SCRIPT = 7; 41 | 42 | public static final String ALL_MIME_TYPES = "*/*"; 43 | private static final String ALL_TEXT_TYPES = "text/*"; 44 | 45 | public static final String MIME_APK = "application/vnd.android.package-archive"; 46 | public static final String MIME_ZIP = "application/zip"; 47 | public static final String MIME_RAR = "application/rar"; 48 | 49 | public static final String MIME_7ZIP = "application/x-7z-compressed"; 50 | 51 | public static final String MIME_BZIP2 = "application/x-bzip2"; 52 | 53 | public static final String MIME_GZIP = "application/x-gzip"; 54 | 55 | public static final String MIME_TAR = "application/x-tar"; 56 | 57 | public static final String MIME_XZ = "application/x-xz"; 58 | 59 | public static final String MIME_ZSTD = "application/zstd"; 60 | 61 | public static final String MIME_LZMA = "application/x-lzma"; 62 | 63 | public static final String MIME_CPIO = "application/x-cpio"; 64 | 65 | public static final String MIME_Z = "application/x-compress"; 66 | 67 | // construct a with an approximation of the capacity 68 | private static final HashMap MIME_TYPES = new HashMap<>(1 + (int) (66 / 0.75)); 69 | 70 | static { 71 | 72 | /* 73 | * ================= MIME TYPES ==================== 74 | */ 75 | MIME_TYPES.put("asm", "text/x-asm"); 76 | MIME_TYPES.put("json", "application/json"); 77 | MIME_TYPES.put("js", "application/javascript"); 78 | 79 | MIME_TYPES.put("def", "text/plain"); 80 | MIME_TYPES.put("in", "text/plain"); 81 | MIME_TYPES.put("list", "text/plain"); 82 | MIME_TYPES.put("log", "text/plain"); 83 | MIME_TYPES.put("pl", "text/plain"); 84 | MIME_TYPES.put("prop", "text/plain"); 85 | MIME_TYPES.put("properties", "text/plain"); 86 | MIME_TYPES.put("rc", "text/plain"); 87 | MIME_TYPES.put("ini", "text/plain"); 88 | MIME_TYPES.put("md", "text/markdown"); 89 | 90 | MIME_TYPES.put("epub", "application/epub+zip"); 91 | MIME_TYPES.put("ibooks", "application/x-ibooks+zip"); 92 | 93 | MIME_TYPES.put("ifb", "text/calendar"); 94 | MIME_TYPES.put("eml", "message/rfc822"); 95 | MIME_TYPES.put("msg", "application/vnd.ms-outlook"); 96 | 97 | MIME_TYPES.put("ace", "application/x-ace-compressed"); 98 | MIME_TYPES.put("7z", "application/x-7z-compressed"); 99 | MIME_TYPES.put("bz", "application/x-bzip"); 100 | MIME_TYPES.put("bz2", "application/x-bzip2"); 101 | MIME_TYPES.put("cab", "application/vnd.ms-cab-compressed"); 102 | MIME_TYPES.put("gz", "application/x-gzip"); 103 | MIME_TYPES.put("lrf", "application/octet-stream"); 104 | MIME_TYPES.put("jar", "application/java-archive"); 105 | MIME_TYPES.put("xz", "application/x-xz"); 106 | MIME_TYPES.put("tar", "application/x-tar"); 107 | MIME_TYPES.put("Z", "application/x-compress"); 108 | MIME_TYPES.put("lzma", "application/x-lzma"); 109 | 110 | MIME_TYPES.put("bat", "application/x-bat"); 111 | MIME_TYPES.put("ksh", "text/plain"); 112 | MIME_TYPES.put("sh", "application/x-sh"); 113 | MIME_TYPES.put("csh", "application/x-csh"); 114 | MIME_TYPES.put("php", "text/x-php"); 115 | MIME_TYPES.put("lisp", "text/x-script.lisp"); 116 | 117 | MIME_TYPES.put("db", "application/octet-stream"); 118 | MIME_TYPES.put("db3", "application/octet-stream"); 119 | 120 | MIME_TYPES.put("otf", "application/x-font-otf"); 121 | MIME_TYPES.put("ttf", "application/x-font-ttf"); 122 | MIME_TYPES.put("psf", "application/x-font-linux-psf"); 123 | 124 | MIME_TYPES.put("cgm", "image/cgm"); 125 | MIME_TYPES.put("btif", "image/prs.btif"); 126 | MIME_TYPES.put("dwg", "image/vnd.dwg"); 127 | MIME_TYPES.put("dxf", "image/vnd.dxf"); 128 | MIME_TYPES.put("fbs", "image/vnd.fastbidsheet"); 129 | MIME_TYPES.put("fpx", "image/vnd.fpx"); 130 | MIME_TYPES.put("fst", "image/vnd.fst"); 131 | MIME_TYPES.put("mdi", "image/vnd.ms-mdi"); 132 | MIME_TYPES.put("npx", "image/vnd.net-fpx"); 133 | MIME_TYPES.put("xif", "image/vnd.xiff"); 134 | MIME_TYPES.put("pct", "image/x-pict"); 135 | MIME_TYPES.put("pic", "image/x-pict"); 136 | 137 | MIME_TYPES.put("adp", "audio/adpcm"); 138 | MIME_TYPES.put("au", "audio/basic"); 139 | MIME_TYPES.put("snd", "audio/basic"); 140 | MIME_TYPES.put("m2a", "audio/mpeg"); 141 | MIME_TYPES.put("m3a", "audio/mpeg"); 142 | MIME_TYPES.put("oga", "audio/ogg"); 143 | MIME_TYPES.put("spx", "audio/ogg"); 144 | MIME_TYPES.put("aac", "audio/x-aac"); 145 | MIME_TYPES.put("mka", "audio/x-matroska"); 146 | 147 | MIME_TYPES.put("jpgv", "video/jpeg"); 148 | MIME_TYPES.put("jpgm", "video/jpm"); 149 | MIME_TYPES.put("jpm", "video/jpm"); 150 | MIME_TYPES.put("mj2", "video/mj2"); 151 | MIME_TYPES.put("mjp2", "video/mj2"); 152 | MIME_TYPES.put("mpa", "video/mpeg"); 153 | MIME_TYPES.put("ogv", "video/ogg"); 154 | MIME_TYPES.put("flv", "video/x-flv"); 155 | MIME_TYPES.put("mkv", "video/x-matroska"); 156 | 157 | } 158 | 159 | 160 | /** 161 | * Get Mime Type of a file 162 | * 163 | * @param name the file of which mime type to get 164 | * @return Mime type in form of String 165 | */ 166 | @NonNull 167 | public static String getMimeType(String name) { 168 | 169 | final String extension = getExtension(name); 170 | 171 | String type = getMimeTypeFromExtension(extension); 172 | // if (type == null) { 173 | // return "application/octet-stream"; 174 | // } 175 | return type == null ? "application/octet-stream" : type; 176 | } 177 | 178 | public static String getMimeTypeFromExtension(String extension) { 179 | String mimeType = null; 180 | // mapping extension to system mime types 181 | if (!extension.isEmpty()) { 182 | final String extensionLowerCase = extension.toLowerCase(); 183 | mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extensionLowerCase); 184 | if (mimeType == null) { 185 | mimeType = MIME_TYPES.get(extensionLowerCase); 186 | } 187 | } 188 | return mimeType; 189 | } 190 | 191 | @MimeType 192 | public static int getSupportMimeType(String name) { 193 | if (isAPK(name)) { 194 | return APK; 195 | } else if (isImage(name)) { 196 | return IMAGE; 197 | } else if (isMusic(name)) { 198 | return MUSIC; 199 | } else if (isMovie(name)) { 200 | return MOVIE; 201 | } else if (isText(name)) { 202 | return TEXT; 203 | } else if (isArchive(name)) { 204 | return ARCHIVE; 205 | } else if (isScript(name)) { 206 | return SCRIPT; 207 | } else { 208 | return OTHER; 209 | } 210 | } 211 | 212 | public static boolean isAPK(String name) { 213 | return APK_PATTERN.matcher(name).matches(); 214 | } 215 | 216 | public static boolean isImage(String name) { 217 | return IMAGE_PATTERN.matcher(name).matches(); 218 | } 219 | 220 | public static boolean isText(String name) { 221 | return TEXT_PATTERN.matcher(name).matches(); 222 | } 223 | 224 | public static boolean isMusic(String name) { 225 | return MUSIC_PATTERN.matcher(name).matches(); 226 | } 227 | 228 | public static boolean isMovie(String name) { 229 | return MOVIE_PATTERN.matcher(name).matches(); 230 | } 231 | 232 | public static boolean isArchive(String name) { 233 | return ARCHIVE_PATTERN.matcher(name).matches(); 234 | } 235 | 236 | public static boolean isScript(String name) { 237 | return SCRIPT_PATTERN.matcher(name).matches(); 238 | } 239 | 240 | public static boolean isShellScript(String name) { 241 | return SHELL_SCRIPT_PATTERN.matcher(name).matches(); 242 | } 243 | 244 | 245 | /** 246 | * Helper method for {@link #getMimeType(String)} 247 | * to calculate the last '.' extension of files 248 | * 249 | * @param name the path of file 250 | * @return extension extracted from name in lowercase 251 | */ 252 | @NonNull 253 | public static String getExtension(@NonNull String name) { 254 | int index = name.lastIndexOf('.'); 255 | /*.开头的文件不算后缀*/ 256 | return index > 0 ? name.substring(index + 1).toLowerCase() : ""; 257 | } 258 | 259 | } 260 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/util/Precondition.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.util; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | /** 6 | * Created by mao on 18-2-24. 7 | */ 8 | 9 | public class Precondition { 10 | 11 | private Precondition() { 12 | } 13 | 14 | public static void checkArgument(boolean expression) { 15 | if (!expression) { 16 | throw new IllegalArgumentException(); 17 | } 18 | } 19 | 20 | public static void checkArgument(boolean expression, @Nullable Object errorMessage) { 21 | if (!expression) { 22 | throw new IllegalArgumentException(String.valueOf(errorMessage)); 23 | } 24 | } 25 | 26 | public static void checkArgument(boolean expression, @Nullable String errorMessageTemplate, @Nullable Object... errorMessageArgs) { 27 | if (!expression) { 28 | throw new IllegalArgumentException(format(errorMessageTemplate, errorMessageArgs)); 29 | } 30 | } 31 | 32 | public static void checkState(boolean expression) { 33 | if (!expression) { 34 | throw new IllegalStateException(); 35 | } 36 | } 37 | 38 | public static void checkState(boolean expression, @Nullable Object errorMessage) { 39 | if (!expression) { 40 | throw new IllegalStateException(String.valueOf(errorMessage)); 41 | } 42 | } 43 | 44 | public static void checkState(boolean expression, @Nullable String errorMessageTemplate, @Nullable Object... errorMessageArgs) { 45 | if (!expression) { 46 | throw new IllegalStateException(format(errorMessageTemplate, errorMessageArgs)); 47 | } 48 | } 49 | 50 | public static T checkNotNull(T reference) { 51 | if (reference == null) { 52 | throw new NullPointerException(); 53 | } else { 54 | return reference; 55 | } 56 | } 57 | 58 | public static T checkNotNull(T reference, @Nullable Object errorMessage) { 59 | if (reference == null) { 60 | throw new NullPointerException(String.valueOf(errorMessage)); 61 | } else { 62 | return reference; 63 | } 64 | } 65 | 66 | public static T checkNotNull(T reference, @Nullable String errorMessageTemplate, @Nullable Object... errorMessageArgs) { 67 | if (reference == null) { 68 | throw new NullPointerException(format(errorMessageTemplate, errorMessageArgs)); 69 | } else { 70 | return reference; 71 | } 72 | } 73 | 74 | public static int checkElementIndex(int index, int size) { 75 | return checkElementIndex(index, size, "index"); 76 | } 77 | 78 | public static int checkElementIndex(int index, int size, @Nullable String desc) { 79 | if (index >= 0 && index < size) { 80 | return index; 81 | } else { 82 | throw new IndexOutOfBoundsException(badElementIndex(index, size, desc)); 83 | } 84 | } 85 | 86 | private static String badElementIndex(int index, int size, String desc) { 87 | if (index < 0) { 88 | return format("%s (%s) must not be negative", new Object[]{desc, Integer.valueOf(index)}); 89 | } else if (size < 0) { 90 | throw new IllegalArgumentException((new StringBuilder(26)).append("negative size: ").append(size).toString()); 91 | } else { 92 | return format("%s (%s) must be less than size (%s)", new Object[]{desc, Integer.valueOf(index), Integer.valueOf(size)}); 93 | } 94 | } 95 | 96 | public static int checkPositionIndex(int index, int size) { 97 | return checkPositionIndex(index, size, "index"); 98 | } 99 | 100 | public static int checkPositionIndex(int index, int size, @Nullable String desc) { 101 | if (index >= 0 && index <= size) { 102 | return index; 103 | } else { 104 | throw new IndexOutOfBoundsException(badPositionIndex(index, size, desc)); 105 | } 106 | } 107 | 108 | private static String badPositionIndex(int index, int size, String desc) { 109 | if (index < 0) { 110 | return format("%s (%s) must not be negative", new Object[]{desc, Integer.valueOf(index)}); 111 | } else if (size < 0) { 112 | throw new IllegalArgumentException((new StringBuilder(26)).append("negative size: ").append(size).toString()); 113 | } else { 114 | return format("%s (%s) must not be greater than size (%s)", new Object[]{desc, Integer.valueOf(index), Integer.valueOf(size)}); 115 | } 116 | } 117 | 118 | public static void checkPositionIndexes(int start, int end, int size) { 119 | if (start < 0 || end < start || end > size) { 120 | throw new IndexOutOfBoundsException(badPositionIndexes(start, end, size)); 121 | } 122 | } 123 | 124 | private static String badPositionIndexes(int start, int end, int size) { 125 | return start >= 0 && start <= size ? (end >= 0 && end <= size ? format("end index (%s) must not be less than execute index (%s)", new Object[]{Integer.valueOf(end), Integer.valueOf(start)}) : badPositionIndex(end, size, "end index")) : badPositionIndex(start, size, "execute index"); 126 | } 127 | 128 | private static String format(String template, @Nullable Object... args) { 129 | template = String.valueOf(template); 130 | StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); 131 | int templateStart = 0; 132 | 133 | int i; 134 | int placeholderStart; 135 | for (i = 0; i < args.length; templateStart = placeholderStart + 2) { 136 | placeholderStart = template.indexOf("%s", templateStart); 137 | if (placeholderStart == -1) { 138 | break; 139 | } 140 | 141 | builder.append(template.substring(templateStart, placeholderStart)); 142 | builder.append(args[i++]); 143 | } 144 | 145 | builder.append(template.substring(templateStart)); 146 | if (i < args.length) { 147 | builder.append(" ["); 148 | builder.append(args[i++]); 149 | 150 | while (i < args.length) { 151 | builder.append(", "); 152 | builder.append(args[i++]); 153 | } 154 | 155 | builder.append(']'); 156 | } 157 | 158 | return builder.toString(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/com/github/maoabc/util/RecyclerViewAdapterChangedCallback.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc.util; 2 | 3 | import androidx.databinding.ObservableList; 4 | import androidx.recyclerview.widget.RecyclerView; 5 | 6 | import java.lang.ref.WeakReference; 7 | 8 | public class RecyclerViewAdapterChangedCallback extends ObservableList.OnListChangedCallback { 9 | private final WeakReference adapterWeakReference; 10 | 11 | public RecyclerViewAdapterChangedCallback(RecyclerView.Adapter adapter) { 12 | this.adapterWeakReference = new WeakReference<>(adapter); 13 | } 14 | 15 | @Override 16 | public void onChanged(ObservableList sender) { 17 | RecyclerView.Adapter adapter = adapterWeakReference.get(); 18 | if (adapter != null) { 19 | adapter.notifyDataSetChanged(); 20 | } 21 | 22 | } 23 | 24 | @Override 25 | public void onItemRangeChanged(ObservableList sender, int positionStart, int itemCount) { 26 | RecyclerView.Adapter adapter = adapterWeakReference.get(); 27 | if (adapter != null) { 28 | adapter.notifyItemRangeChanged(positionStart, itemCount); 29 | } 30 | 31 | } 32 | 33 | @Override 34 | public void onItemRangeInserted(ObservableList sender, int positionStart, int itemCount) { 35 | RecyclerView.Adapter adapter = adapterWeakReference.get(); 36 | if (adapter != null) { 37 | adapter.notifyItemRangeInserted(positionStart, itemCount); 38 | } 39 | 40 | } 41 | 42 | @Override 43 | public void onItemRangeMoved(ObservableList sender, int fromPosition, int toPosition, int itemCount) { 44 | RecyclerView.Adapter adapter = adapterWeakReference.get(); 45 | if (adapter != null) { 46 | for (int i = 0; i < itemCount; i++) { 47 | adapter.notifyItemMoved(fromPosition + i, toPosition + i); 48 | } 49 | } 50 | 51 | } 52 | 53 | @Override 54 | public void onItemRangeRemoved(ObservableList sender, int positionStart, int itemCount) { 55 | RecyclerView.Adapter adapter = adapterWeakReference.get(); 56 | if (adapter != null) { 57 | adapter.notifyItemRangeRemoved(positionStart, itemCount); 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/res/color/checked_button_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/drawable-hdpi/ic_stat_terminal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/drawable-mdpi/ic_stat_terminal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/drawable-xhdpi/ic_stat_terminal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/drawable-xxhdpi/ic_stat_terminal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_stat_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/drawable-xxxhdpi/ic_stat_terminal.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_24dp.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_back_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_downward_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_forward_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_upward_24dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_cancel_24dp.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/side_nav_bar.xml: -------------------------------------------------------------------------------- 1 | 3 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_aterm.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 16 | 17 | 18 | 28 | 29 | 37 | 38 | 45 | 46 | 52 | 53 | 57 | 58 | 59 | 67 | 68 | 75 | 76 | 83 | 84 | 91 | 92 | 99 | 100 | 107 | 108 | 115 | 116 | 117 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 136 | 137 | 140 | 141 | 147 | 148 | 156 | 157 | 173 | 174 | 175 | 183 | 184 | 192 | 193 | 194 | 195 | 196 | 203 | 204 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_aterm_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 21 | 22 | 23 | 24 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_create_ssh_server.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 17 | 18 | 21 | 22 | 29 | 30 | 36 | 37 | 38 | 46 | 47 | 54 | 55 | 56 | 57 | 65 | 66 | 72 | 73 | 74 | 83 | 84 | 94 | 95 | 101 | 102 | 103 | 104 | 118 | 119 | 120 | 130 | 131 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_text_field.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 16 | 17 | 18 | 19 | 29 | 30 | 39 | 40 | 51 | 52 | -------------------------------------------------------------------------------- /app/src/main/res/layout/recycler_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/terminal_list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 13 | 14 | 17 | 18 | 19 | 28 | 29 | 44 | 45 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 8 | 9 9 | 10 10 | 12 11 | 14 12 | 16 13 | 20 14 | 24 15 | 28 16 | 32 17 | 36 18 | 42 19 | 48 20 | 21 | 22 | 23 | Black text on white 24 | White text on black 25 | White text on blue 26 | Green text on black 27 | Amber text on black 28 | Red text on black 29 | Holo blue text on black 30 | Solarized Light 31 | Solarized Dark 32 | Linux Console 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/values/arraysNoLocalize.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 7 | 9 8 | 10 9 | 12 10 | 14 11 | 16 12 | 20 13 | 24 14 | 28 15 | 32 16 | 36 17 | 42 18 | 48 19 | 20 | 21 | 22 | 0 23 | 1 24 | 2 25 | 3 26 | 4 27 | 5 28 | 6 29 | 7 30 | 8 31 | 9 32 | 33 | 34 | 35 | 36 | 0 37 | 1 38 | 2 39 | 3 40 | 4 41 | 5 42 | 6 43 | 7 44 | 45 | 46 | 47 | 0 48 | 1 49 | 2 50 | 3 51 | 4 52 | 5 53 | 6 54 | 7 55 | 56 | 57 | 58 | vt100 59 | screen 60 | linux 61 | screen-256color 62 | xterm 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | #000000 7 | #8A000000 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 36dp 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ATerm 3 | Run Script 4 | 5 | View 6 | Color 7 | Background Alpha 8 | 9 | Font Size 10 | Font size 11 | New Terminal 12 | Close 13 | New SSH 14 | Edit SSH 15 | Edit session name 16 | OK 17 | Cancel 18 | Text is empty 19 | Close terminal session 20 | Terminal session running 21 | [Process complete(%d) - press Enter] 22 | Host 23 | Port 24 | Username 25 | Password 26 | Use key 27 | Private key content 28 | Key path 29 | Key passphrase 30 | Host is empty 31 | Username is empty 32 | Need password or key 33 | Key format invalid 34 | Terminal settings 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 22 | 23 | 32 | 33 | 44 | 45 | 56 | 57 | 58 | 70 | 71 | 48dp 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/res/xml/aterm_preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 10 | 17 | 18 | 25 | 26 | 32 | 33 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_descriptor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/test/java/com/github/maoabc/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.github.maoabc; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:4.1.1' 12 | 13 | 14 | // NOTE: Do not place your application dependencies here; they belong 15 | // in the individual module build.gradle files 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | jcenter() 23 | 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maoabc/aterminal/a527c7ee80eec0d2f1a25fb6e87167e88fa68838/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Jun 27 20:21:54 CST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name='aterm' 2 | include ':app' 3 | include ':terminalview' 4 | include ':pty' 5 | --------------------------------------------------------------------------------