commit 005d65330de45640c64ca38c7441bdf407a71a12 Author: liuguijing <123456> Date: Mon Jun 10 22:10:55 2024 +0800 初次提交 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..977b79d --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,107 @@ +plugins { + id 'com.android.application' +} + +android { + namespace 'com.xypower.wpywapp' + compileSdk 33 + + defaultConfig { + applicationId "com.xypower.wpywapp" + minSdk 28 + targetSdk 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + signingConfigs { + debug { + storeFile file("../sign/platform.jks") + storePassword 'dowse_mtk8788' + keyAlias 'ds_mt8788_key' + keyPassword 'dowse_mtk8788' + } + + release { + storeFile file("../sign/platform.jks") + storePassword 'dowse_mtk8788' + keyAlias 'ds_mt8788_key' + keyPassword 'dowse_mtk8788' + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + /* +*支持databinding +* */ + dataBinding { + enabled = true + } + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.0")) + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.6.1' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' + implementation 'androidx.navigation:navigation-fragment:2.5.3' + implementation 'androidx.navigation:navigation-ui:2.5.3' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.5' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + implementation 'me.jessyan:autosize:1.2.1' + + //viewmodel依赖 + implementation "androidx.lifecycle:lifecycle-viewmodel:2.4.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" +//Livedata去除粘性事件 + implementation 'com.kunminx.arch:unpeek-livedata:7.2.0-beta1' + + implementation "com.tananaev:adblib:1.3" + + + + // Add this library +// implementation 'com.github.MuntashirAkon:libadb-android:1.0.1' + + // Library to generate X509Certificate. You can also use BouncyCastle for + // this. See example for use-case. + implementation 'com.github.MuntashirAkon:sun-security-android:1.1' + + // Bypass hidden API if you want to use the Android default Conscrypt in + // Android 9 (Pie) or later. It also requires additional steps. See + // https://github.com/LSPosed/AndroidHiddenApiBypass to find out more about + // this. + implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:2.0' + + // Use custom Conscrypt library. If you want to connect to a remote ADB + // daemon instead of the device the app is currently running or do not want + // to bypass hidden API, this is the recommended choice. + implementation 'org.conscrypt:conscrypt-android:2.5.2' + implementation 'com.github.MuntashirAkon.spake2-java:android:2.0.0' + implementation 'org.bouncycastle:bcprov-jdk15to18:1.78' + + implementation 'com.tananaev:adblib:1.3' + + + api 'com.github.KunMinX:MVI-Dispatcher:7.6.0' +// api 'com.github.KunMinX.Strict-DataBinding:strict_databinding:6.2.0' + + //Livedata去除粘性事件 + implementation 'com.kunminx.arch:unpeek-livedata:7.8.0' +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java b/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..8f6add9 --- /dev/null +++ b/app/src/androidTest/java/com/xypower/wpywapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.xypower.wpywapp; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.xypower.wpywapp", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f9058c8 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ADBManager.java b/app/src/main/java/com/xypower/wpywapp/ADBManager.java new file mode 100644 index 0000000..3def1d6 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ADBManager.java @@ -0,0 +1,20 @@ +package com.xypower.wpywapp; + +public class ADBManager { + private static final String ADB_PATH = "/path/to/adb"; // 指定adb的路径 + + public static void connect() { + // 执行adb连接命令 + executeCommand(ADB_PATH + " connect :"); + } + + public static void disconnect() { + // 执行adb断开连接命令 + executeCommand(ADB_PATH + " disconnect :"); + } + + private static void executeCommand(String command) { + // 执行adb命令的逻辑 + + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/MainActivity.java b/app/src/main/java/com/xypower/wpywapp/MainActivity.java new file mode 100644 index 0000000..54583fd --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/MainActivity.java @@ -0,0 +1,398 @@ +package com.xypower.wpywapp; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; + +import android.content.Intent; +import android.location.Location; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import com.tananaev.adblib.AdbConnection; +import com.tananaev.adblib.AdbCrypto; +import com.tananaev.adblib.AdbStream; +import com.xypower.wpywapp.bean.TerminalBean; +import com.xypower.wpywapp.databinding.ActivityMainBinding; +import com.xypower.wpywapp.interfaces.MainActCallback; +import com.xypower.wpywapp.jadb.JadbDevice; +import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbConnectionManager; +import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbInputStream; +import com.xypower.wpywapp.tool.LocationUtil; +import com.xypower.wpywapp.ui.BottomActivity; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.Socket; + + +/* +这三个文件夹下日志的清除 +* /var/log/xymp/xymanagerLogs +* /var/log/xymp/logs +* /usr/local/share/tomcat/logs/ + + + +这个文件日志的切割 +/usr/local/share/tomcat/logs/catalina.out +* +* */ +public class MainActivity extends AppCompatActivity { + + private JadbDevice jadbDevice; + private TerminalBean bean = new TerminalBean(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); + bean.setVerion("未知"); + bean.setSim("未知"); + bean.setPower("未知"); + binding.setBean(bean); + Handler handler = new Handler() { + @Override + public void handleMessage(@NonNull Message msg) { + super.handleMessage(msg); + if (msg.what == 1) { + Toast.makeText(MainActivity.this, "1111", Toast.LENGTH_SHORT).show(); + } else if (msg.what == 2) { + Toast.makeText(MainActivity.this, "2222", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(MainActivity.this, "3333", Toast.LENGTH_SHORT).show(); + } + } + }; + binding.setCallback(new MainActCallback() { + @Override + public void link(View view) { + + LocationUtil.register(MainActivity.this, 0, 0, new LocationUtil.OnLocationChangeListener() { + @Override + public void getLastKnownLocation(Location location) { + Log.e("xyh", "onLocationChanged: " + location.getLatitude()); + } + + @Override + public void onLocationChanged(Location location) { + //位置信息变化时触发 + Log.e("xyh", "定位方式:" + location.getProvider()); + Log.e("xyh", "纬度:" + location.getLatitude()); + Log.e("xyh", "经度:" + location.getLongitude()); + Log.e("xyh", "海拔:" + location.getAltitude()); + Log.e("xyh", "时间:" + location.getTime()); + Log.e("xyh", "国家:" + LocationUtil.getCountryName(MainActivity.this, location.getLatitude(), location.getLongitude())); + Log.e("xyh", "获取地理位置:" + LocationUtil.getAddress(MainActivity.this, location.getLatitude(), location.getLongitude())); + Log.e("xyh", "所在地:" + LocationUtil.getLocality(MainActivity.this, location.getLatitude(), location.getLongitude())); + Log.e("xyh", "所在街道:" + LocationUtil.getStreet(MainActivity.this, location.getLatitude(), location.getLongitude())); + + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + System.out.println("dfsad"); + } + }); + + + //连接设备 + +// Intent intent = new Intent(MainActivity.this, BottomActivity.class); +// startActivity(intent); +// String s = binding.ip.getText().toString(); +// if (RegexUtil.checkIpAddress(s)) { + //获取可adb调试的设备列表 +// new Thread(new Runnable() { +// @Override +// public void run() { +//// try { +////// Process process = Runtime.getRuntime().exec("adb shell devices"); +////// int i = process.waitFor(); +////// if (i == 0) { +////// InputStream inputStream = process.getInputStream(); +////// System.out.println("2312"); +////// } else { +////// InputStream errorStream = process.getErrorStream(); +////// System.out.println("888484"); +////// } +////// AdbServer adbServer = new AdbServer(new AdbResponder() { +////// @Override +////// public void onCommand(String command) { +////// Toast.makeText(MainActivity.this,"cehgnogn1",Toast.LENGTH_SHORT).show(); +////// } +////// +////// @Override +////// public int getVersion() { +////// Toast.makeText(MainActivity.this,"cehgnogn3",Toast.LENGTH_SHORT).show(); +////// return 0; +////// } +////// +////// @Override +////// public List getDevices() { +////// Toast.makeText(MainActivity.this,"cehgnogn2",Toast.LENGTH_SHORT).show(); +////// return null; +////// } +////// }); +////// Runnable responder = adbServer.createResponder(new Socket("127.0.0.1", 15037)); +////// responder.run(); +////// JadbConnection jadb = new JadbConnection("192.168.0.100",5555); +//// JadbConnection jadb = new JadbConnection("192.168.0.118", 5555); +////// JadbConnection jadb = new JadbConnection("127.0.0.1", 5555); +////// InetSocketAddress inetSocketAddress = jadb.connectToTcpDevice(new InetSocketAddress("192.168.0.101",5555)); +////// Message msg1 = new Message(); +////// msg1.what = 3; +////// handler.sendMessage(msg1); +////// System.out.println(inetSocketAddress); +//// List devices = jadb.getDevices(); +//// jadbDevice = devices.get(0); +////// jadbDevice.pull(); +//// } catch (IOException e) { +//// throw new RuntimeException(e); +//// } catch (JadbException e) { +//// throw new RuntimeException(e); +////// } catch (ConnectionToRemoteDeviceException e) { +////// throw new RuntimeException(e); +//// } +// +// +// +//// JadbConnection jadbConnection = new JadbConnection(); +//// List anyDevice = null; +//// try { +//// InetSocketAddress inetSocketAddress = jadbConnection.connectToTcpDevice(new InetSocketAddress("192.168.0.118",5555)); +//// List devices = jadbConnection.getDevices(); +//// JadbDevice jadbDevice = devices.get(1); +//// RemoteFile remoteFile = new RemoteFile("/storage/emulated/0/iport_log.txt"); +//// String absolutePath = getFilesDir().getAbsolutePath(); +//// jadbDevice.pull(remoteFile,new File(absolutePath+"/a.txt")); +//// System.out.println(inetSocketAddress); +//// System.out.println(devices); +////// anyDevice = jadbConnection.getDevices(); +//// } catch (IOException e) { +//// e.printStackTrace(); +//// } catch (JadbException e) { +//// e.printStackTrace(); +////// } catch (ConnectionToRemoteDeviceException e) { +//// e.printStackTrace(); +//// } catch (ConnectionToRemoteDeviceException e) { +//// e.printStackTrace(); +//// +//// } +// +// +//// try { +//// if(Runtime.getRuntime().exec(new String[]{"adb", "version"}).waitFor()!=0){ +//// System.out.println("dfsd"); +//// } +//// } catch (IOException e) { +//// throw new RuntimeException(e); +//// } catch (InterruptedException e) { +//// throw new RuntimeException(e); +//// } +// +// +// } +// }).start(); +// } else { +// Toast.makeText(MainActivity.this, "IP不合法", Toast.LENGTH_SHORT).show(); +// } +// asyncTask.execute(); + } + + @Override + public void getInfo(View view) { + + new Thread(new Runnable() { + @Override + public void run() { +// ADBService adbService = new ADBServiceImpl(); +//// adbService.pullFile() +// try { +// List connectedDevicesUdid = adbService.getConnectedDevicesUdid(); +// System.out.println(connectedDevicesUdid); +// } catch (Exception e) { +// throw new RuntimeException(e); +// } + + + try { + boolean connect = AdbConnectionManager.getInstance(MainActivity.this).connect("192.168.50.109", 5555); + if (connect) { +// /storage/emulated/0/i1/log/2024_04_21_yunWei.log + String absolutePath = getFilesDir().getAbsolutePath(); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream1 = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: exit"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("pull: /storage/emulated/0/i1/log/2024_04_21_yunWei.log "+absolutePath+"/"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: date"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbConnection adbConnection = AdbConnectionManager.getInstance(MainActivity.this).getAdbConnection(); +// ("pull: /storage/emulated/0/i1/log/2024_04_21_yunWei.log "+absolutePath+"/"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream open = adbConnection.open("sync: recv /storage/emulated/0/i1/iport_log.txt "+absolutePath+"/"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync: recv: /storage/emulated/0/i1/iport_log.txt "+absolutePath+"/"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cat /storage/emulated/0/iport_log.txt"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cat /storage/emulated/0/iport_log.txt"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell:cp /storage/emulated/0/iport_log.txt "+absolutePath+"/aa.txt"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream2 = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream1 = AdbConnectionManager.getInstance(MainActivity.this).openStream(LocalServices.SYNC); +// adbStream1.flush(); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("shell: sync send /storage/emulated/0/ "+absolutePath+"/324.txt"); +// com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:"+"\n" +"recv: /storage/emulated/0/i1/iport_log.txt"); + com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.AdbStream adbStream = AdbConnectionManager.getInstance(MainActivity.this).openStream("sync:"+"\n" +"recv: /storage/emulated/0/i1/iport_log.txt"); +// byte[] b = new byte[4]; +// AdbInputStream adbInputStream = adbStream.openInputStream(); + AdbInputStream adbInputStream = adbStream.openInputStream(); + +// + + String result = null; + BufferedReader reader = new BufferedReader(new InputStreamReader(adbInputStream)); + String line = null; + StringBuffer sb = new StringBuffer(); + while ((line = reader.readLine()) != null) { + sb.append(line); + } + result = sb.toString(); + +// int off = 0; +// int n = 0; +// int len = b.length; +// while (n < len) { +// int count = adbInputStream.read(b, off + n, len - n); +// if (count < 0) +// throw new EOFException(); +// n += count; +// } + + System.out.println(result); + } + } catch (Exception e) { + try { + AdbConnectionManager.getInstance(MainActivity.this).disconnect(); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + throw new RuntimeException(e); + } + +// try { +// Process process = Runtime.getRuntime().exec("version"); +// int i = process.waitFor(); +// if (i == 0) { +// InputStream inputStream = process.getInputStream(); +// System.out.println("2312"); +// } else { +// InputStream errorStream = process.getErrorStream(); +// System.out.println("888484"); +// } +//// JadbConnection jadb = new JadbConnection(s, 5555); +//// List devices = jadb.getDevices(); +//// jadbDevice = devices.get(0); +// } catch (IOException e) { +// throw new RuntimeException(e); +// } catch (InterruptedException e) { +// throw new RuntimeException(e); +// } + } + }).start(); + + //获取设备信息 +// try { +// if (jadbDevice != null) { +// jadbDevice.pull(new RemoteFile("/path/to/file.txt"), new File("file.txt")); +// //解析文件 将文件的值赋给bean对象 +// binding.setBean(bean); +// } +// } catch (IOException e) { +// throw new RuntimeException(e); +// } catch (JadbException e) { +// throw new RuntimeException(e); +// } + } + + @Override + public void takePic(View view) { + + new Thread(new Runnable() { + @Override + public void run() { + + + Socket socket = null; + AdbCrypto crypto = new AdbCrypto(); + AdbConnection adbConnection = null; + try { +// Message msg = new Message(); +// msg.what = 1; +// handler.sendMessage(msg); + socket = new Socket("192.168.0.101", 5037); +// Message msg1 = new Message(); +// msg1.what = 3; +// handler.sendMessage(msg1); +// handler.sendMessage(new Message()); + adbConnection = AdbConnection.create(socket, crypto); + adbConnection.connect(); + Message msg1 = new Message(); + msg1.what = 3; + handler.sendMessage(msg1); + System.out.println("dsadADas"); + AdbStream fsda = adbConnection.open("devices"); + System.out.println("rewrewrew"); + } catch (IOException e) { + Message msg = new Message(); + msg.what = 2; + handler.sendMessage(msg); +// throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + }).start(); + + +// CommandResult adbDevices = ShellUtils.execCommand("adb version", false); +// System.out.println(adbDevices.errorMsg); +// testCmd.runAdbCommand("version"); +// System.out.println(s); +// boolean b = testCmd.haveRoot(); +// if (b) { +// +// } +// if (jadbDevice != null) { + //手动拍照 +// jadbDevice.buildCmdLine("手动拍照"); +// } + } + }); + + + } + + public boolean runRootCommand(String command) { + Process process = null; + DataOutputStream os = null; + try { + process = Runtime.getRuntime().exec("su"); + os = new DataOutputStream(process.getOutputStream()); + os.writeBytes(command + "\n"); + os.writeBytes("exit\n"); + os.flush(); + process.waitFor(); + } catch (Exception e) { + return false; + } finally { + try { + if (os != null) { + os.close(); + } + process.destroy(); + } catch (Exception e) { + } + } + return true; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/WpywApplication.java b/app/src/main/java/com/xypower/wpywapp/WpywApplication.java new file mode 100644 index 0000000..c86c30b --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/WpywApplication.java @@ -0,0 +1,47 @@ +package com.xypower.wpywapp; + +import android.app.ActivityManager; +import android.app.Application; +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModelStore; +import androidx.lifecycle.ViewModelStoreOwner; + +public class WpywApplication extends Application implements ViewModelStoreOwner { + private ViewModelStore mAppViewModelStore; + + @Override + public void onCreate() { + super.onCreate(); + mAppViewModelStore = new ViewModelStore(); + } + + @NonNull + @Override + public ViewModelStore getViewModelStore() { + return mAppViewModelStore; + } + + /** + * 判断是否是UI进程 + * + * @return + */ + public boolean isUIProcess() { + int pid = android.os.Process.myPid(); + String processName = ""; + ActivityManager activityManager = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE); + for (ActivityManager.RunningAppProcessInfo runningAppProcessInfo : activityManager.getRunningAppProcesses()) { + if (runningAppProcessInfo.pid == pid) { + processName = runningAppProcessInfo.processName; + break; + } + } + return TextUtils.equals(getPackageName(), processName); + } + + +} diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java b/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java new file mode 100644 index 0000000..d8b6c38 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/base/BaseActivity.java @@ -0,0 +1,82 @@ +/* + * Copyright 2018-present KunMinX + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xypower.wpywapp.base; + +import android.app.Activity; +import android.content.Intent; +import android.content.res.Resources; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModel; + +import com.kunminx.architecture.ui.scope.ViewModelScope; +import com.xypower.wpywapp.page.DataBindingActivity; + +/** + * Create by KunMinX at 19/8/1 + */ +public abstract class BaseActivity extends DataBindingActivity { + + private final ViewModelScope mViewModelScope = new ViewModelScope(); + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + + + super.onCreate(savedInstanceState); + + + //TODO tip 1: DataBinding 严格模式(详见 DataBindingActivity - - - - - ): + // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, + // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, + // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 + + // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + } + + //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控, + //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域, + //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个, + //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。 + + //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840 + + protected T getActivityScopeViewModel(@NonNull Class modelClass) { + return mViewModelScope.getActivityScopeViewModel(this, modelClass); + } + + protected T getApplicationScopeViewModel(@NonNull Class modelClass) { + return mViewModelScope.getApplicationScopeViewModel(modelClass); + } + + + protected void toggleSoftInput() { + InputMethodManager imm = (InputMethodManager) getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); + } + + protected void openUrlInBrowser(String url) { + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java b/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java new file mode 100644 index 0000000..9bcbaf5 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/base/BaseFragment.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018-present KunMinX + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.xypower.wpywapp.base; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.view.inputmethod.InputMethodManager; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.navigation.NavController; +import androidx.navigation.fragment.NavHostFragment; + +import com.kunminx.architecture.ui.scope.ViewModelScope; +import com.xypower.wpywapp.page.DataBindingFragment; + +/** + * Create by KunMinX at 19/7/11 + */ +public abstract class BaseFragment extends DataBindingFragment { + + private final ViewModelScope mViewModelScope = new ViewModelScope(); + + //TODO tip 1: DataBinding 严格模式(详见 DataBindingFragment - - - - - ): + // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, + // 通过这方式,彻底解决 View 实例 Null 安全一致性问题, + // 如此,View 实例 Null 安全性将和基于函数式编程思想的 Jetpack Compose 持平。 + + // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + + //TODO tip 2: Jetpack 通过 "工厂模式" 实现 ViewModel 作用域可控, + //目前我们在项目中提供了 Application、Activity、Fragment 三个级别的作用域, + //值得注意的是,通过不同作用域 Provider 获得 ViewModel 实例非同一个, + //故若 ViewModel 状态信息保留不符合预期,可从该角度出发排查 是否眼前 ViewModel 实例非目标实例所致。 + + //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6257931840 + + protected T getFragmentScopeViewModel(@NonNull Class modelClass) { + return mViewModelScope.getFragmentScopeViewModel(this, modelClass); + } + + protected T getActivityScopeViewModel(@NonNull Class modelClass) { + return mViewModelScope.getActivityScopeViewModel(mActivity, modelClass); + } + + protected T getApplicationScopeViewModel(@NonNull Class modelClass) { + return mViewModelScope.getApplicationScopeViewModel(modelClass); + } + + protected NavController nav() { + return NavHostFragment.findNavController(this); + } + + protected void toggleSoftInput() { + InputMethodManager imm = (InputMethodManager) mActivity.getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.toggleSoftInput(0, InputMethodManager.HIDE_NOT_ALWAYS); + } + + protected void openUrlInBrowser(String url) { + Uri uri = Uri.parse(url); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + startActivity(intent); + } + + protected Context getApplicationContext() { + return mActivity.getApplicationContext(); + } + +} diff --git a/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java b/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java new file mode 100644 index 0000000..f949398 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/base/BaseViewModel.java @@ -0,0 +1,10 @@ +package com.xypower.wpywapp.base; + +import androidx.lifecycle.ViewModel; + +public class BaseViewModel extends ViewModel { + @Override + protected void onCleared() { + super.onCleared(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java b/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java new file mode 100644 index 0000000..0c142b8 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/bean/TerminalBean.java @@ -0,0 +1,32 @@ +package com.xypower.wpywapp.bean; + +public class TerminalBean { + + private String verion; + private String sim; + private String power; + + public String getVerion() { + return verion; + } + + public void setVerion(String verion) { + this.verion = verion; + } + + public String getSim() { + return sim; + } + + public void setSim(String sim) { + this.sim = sim; + } + + public String getPower() { + return power; + } + + public void setPower(String power) { + this.power = power; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java b/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java new file mode 100644 index 0000000..1fdbb47 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/interfaces/MainActCallback.java @@ -0,0 +1,15 @@ +package com.xypower.wpywapp.interfaces; + +import android.view.View; + +import java.io.IOException; + +public interface MainActCallback { + + void link(View view); + + void getInfo(View view); + + void takePic(View view); + +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java new file mode 100644 index 0000000..05f1da4 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterInputStream.java @@ -0,0 +1,47 @@ +package com.xypower.wpywapp.jadb; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class AdbFilterInputStream extends FilterInputStream { + public AdbFilterInputStream(InputStream inputStream) { + super(inputStream); + } + + @Override + public int read() throws IOException { + int b1 = in.read(); + if (b1 == 0x0d) { + in.mark(1); + int b2 = in.read(); + if (b2 == 0x0a) { + return b2; + } + in.reset(); + } + return b1; + } + + @Override + public int read(byte[] buffer, int offset, int length) throws IOException { + int n = 0; + for (int i = 0; i < length; i++) { + int b = read(); + if (b == -1) return n == 0 ? -1 : n; + buffer[offset + n] = (byte) b; + n++; + + // Return as soon as no more data is available (and at least one byte was read) + if (in.available() <= 0) { + return n; + } + } + return n; + } + + @Override + public int read(byte[] buffer) throws IOException { + return read(buffer, 0, buffer.length); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java new file mode 100644 index 0000000..d8432d3 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbFilterOutputStream.java @@ -0,0 +1,21 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; +import java.io.OutputStream; + +public class AdbFilterOutputStream extends LookBackFilteringOutputStream { + public AdbFilterOutputStream(OutputStream inner) { + super(inner, 1); + } + + @Override + public void write(int c) throws IOException { + if (!lookback().isEmpty()) { + Byte last = lookback().getFirst(); + if (last != null && last == 0x0d && c == 0x0a) { + unwrite(); + } + } + super.write(c); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java b/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java new file mode 100644 index 0000000..244892c --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/AdbServerLauncher.java @@ -0,0 +1,48 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; +import java.util.Map; + +/** + * Launches the ADB server + */ +public class AdbServerLauncher { + private final String executable; + private Subprocess subprocess; + + /** + * Creates a new launcher loading ADB from the environment. + * + * @param subprocess the sub-process. + * @param environment the environment to use to locate the ADB executable. + */ + public AdbServerLauncher(Subprocess subprocess, Map environment) { + this(subprocess, findAdbExecutable(environment)); + } + + /** + * Creates a new launcher with the specified ADB. + * + * @param subprocess the sub-process. + * @param executable the location of the ADB executable. + */ + public AdbServerLauncher(Subprocess subprocess, String executable) { + this.subprocess = subprocess; + this.executable = executable; + } + + private static String findAdbExecutable(Map environment) { + String androidHome = environment.get("ANDROID_HOME"); + if (androidHome == null || androidHome.equals("")) { + return "adb"; + } + return androidHome + "/platform-tools/adb"; + } + + public void launch() throws IOException, InterruptedException { + Process p = subprocess.execute(new String[]{executable, "start-server"}); + p.waitFor(); + int exitValue = p.exitValue(); + if (exitValue != 0) throw new IOException("adb exited with exit code: " + exitValue); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java b/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java new file mode 100644 index 0000000..5f513c9 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/ConnectionToRemoteDeviceException.java @@ -0,0 +1,7 @@ +package com.xypower.wpywapp.jadb; + +public class ConnectionToRemoteDeviceException extends Exception { + public ConnectionToRemoteDeviceException(String message) { + super(message); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java new file mode 100644 index 0000000..93adb18 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceDetectionListener.java @@ -0,0 +1,9 @@ +package com.xypower.wpywapp.jadb; + +import java.util.List; + +public interface DeviceDetectionListener { + void onDetect(List devices); + void onException(Exception e); +} + diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java new file mode 100644 index 0000000..db57ae8 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/DeviceWatcher.java @@ -0,0 +1,44 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; + +public class DeviceWatcher implements Runnable { + private Transport transport; + private final DeviceDetectionListener listener; + private final JadbConnection connection; + + public DeviceWatcher(Transport transport, DeviceDetectionListener listener, JadbConnection connection) { + this.transport = transport; + this.listener = listener; + this.connection = connection; + } + + @Override + public void run() { + watch(); + } + + @SuppressWarnings("squid:S2189") // watcher is stopped by closing transport + private void watch() { + try { + while (true) { + listener.onDetect(connection.parseDevices(transport.readString())); + } + } catch (IOException ioe) { + synchronized(this) { + if (transport != null) { + listener.onException(ioe); + } + } + } catch (Exception e) { + listener.onException(e); + } + } + + public void stop() throws IOException { + synchronized(this) { + transport.close(); + transport = null; + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java new file mode 100644 index 0000000..4a7cc7c --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectToRemoteTcpDevice.java @@ -0,0 +1,29 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; +import java.net.InetSocketAddress; + +class HostConnectToRemoteTcpDevice extends HostConnectionCommand { + HostConnectToRemoteTcpDevice(Transport transport) { + super(transport, new ResponseValidatorImp()); + } + + //Visible for testing + HostConnectToRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { + super(transport, responseValidator); + } + + InetSocketAddress connect(InetSocketAddress inetSocketAddress) + throws IOException, JadbException, ConnectionToRemoteDeviceException { + return executeHostCommand("connect", inetSocketAddress); + } + + static final class ResponseValidatorImp extends ResponseValidatorBase { + private static final String SUCCESSFULLY_CONNECTED = "connected to"; + private static final String ALREADY_CONNECTED = "already connected to"; + + ResponseValidatorImp() { + super(SUCCESSFULLY_CONNECTED, ALREADY_CONNECTED); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java new file mode 100644 index 0000000..c007df2 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostConnectionCommand.java @@ -0,0 +1,70 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class HostConnectionCommand { + private final Transport transport; + private final ResponseValidator responseValidator; + + HostConnectionCommand(Transport transport, ResponseValidator responseValidator) { + this.transport = transport; + this.responseValidator = responseValidator; + } + + InetSocketAddress executeHostCommand(String command, InetSocketAddress inetSocketAddress) + throws IOException, JadbException, ConnectionToRemoteDeviceException { + transport.send(String.format("host:%s:%s:%d", command, inetSocketAddress.getHostString(), inetSocketAddress.getPort())); + verifyTransportLevel(); + verifyProtocolLevel(); + + return inetSocketAddress; + } + + private void verifyTransportLevel() throws IOException, JadbException { + transport.verifyResponse(); + } + + private void verifyProtocolLevel() throws IOException, ConnectionToRemoteDeviceException { + String status = transport.readString(); + responseValidator.validate(status); + } + + //@VisibleForTesting + interface ResponseValidator { + void validate(String response) throws ConnectionToRemoteDeviceException; + } + + static class ResponseValidatorBase implements ResponseValidator { + private final String successMessage; + private final String errorMessage; + + ResponseValidatorBase(String successMessage, String errorMessage) { + this.successMessage = successMessage; + this.errorMessage = errorMessage; + } + + public void validate(String response) throws ConnectionToRemoteDeviceException { + if (!checkIfConnectedSuccessfully(response) && !checkIfAlreadyConnected(response)) { + throw new ConnectionToRemoteDeviceException(extractError(response)); + } + } + + private boolean checkIfConnectedSuccessfully(String response) { + return response.startsWith(successMessage); + } + + private boolean checkIfAlreadyConnected(String response) { + return response.startsWith(errorMessage); + } + + private String extractError(String response) { + int lastColon = response.lastIndexOf(':'); + if (lastColon != -1) { + return response.substring(lastColon); + } else { + return response; + } + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java new file mode 100644 index 0000000..7e01dc5 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/HostDisconnectFromRemoteTcpDevice.java @@ -0,0 +1,30 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; +import java.net.InetSocketAddress; + +public class HostDisconnectFromRemoteTcpDevice extends HostConnectionCommand { + HostDisconnectFromRemoteTcpDevice(Transport transport) { + super(transport, new ResponseValidatorImp()); + + } + + //Visible for testing + HostDisconnectFromRemoteTcpDevice(Transport transport, ResponseValidator responseValidator) { + super(transport, responseValidator); + } + + InetSocketAddress disconnect(InetSocketAddress inetSocketAddress) + throws IOException, JadbException, ConnectionToRemoteDeviceException { + return executeHostCommand("disconnect", inetSocketAddress); + } + + static final class ResponseValidatorImp extends ResponseValidatorBase { + private static final String SUCCESSFULLY_DISCONNECTED = "disconnected"; + private static final String ALREADY_DISCONNECTED = "error: no such device"; + + ResponseValidatorImp() { + super(SUCCESSFULLY_DISCONNECTED, ALREADY_DISCONNECTED); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java b/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java new file mode 100644 index 0000000..36eba22 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/ITransportFactory.java @@ -0,0 +1,10 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; + +/** + * Created by Törcsi on 2016. 03. 01.. + */ +public interface ITransportFactory { + Transport createTransport() throws IOException; +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java new file mode 100644 index 0000000..881bb8d --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbConnection.java @@ -0,0 +1,83 @@ +package com.xypower.wpywapp.jadb; + + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; + +public class JadbConnection implements ITransportFactory { + + private final String host; + private final int port; + + private static final int DEFAULTPORT = 5037; + + public JadbConnection() { + this("localhost", DEFAULTPORT); + } + + public JadbConnection(String host, int port) { + this.host = host; + this.port = port; + } + + public Transport createTransport() throws IOException { + return new Transport(new Socket(host, port)); + } + + public String getHostVersion() throws IOException, JadbException { + try (Transport transport = createTransport()) { + transport.send("host:version"); + transport.verifyResponse(); + return transport.readString(); + } + } + + public InetSocketAddress connectToTcpDevice(InetSocketAddress inetSocketAddress) + throws IOException, JadbException, ConnectionToRemoteDeviceException { + try (Transport transport = createTransport()) { + return new HostConnectToRemoteTcpDevice(transport).connect(inetSocketAddress); + } + } + + public InetSocketAddress disconnectFromTcpDevice(InetSocketAddress tcpAddressEntity) + throws IOException, JadbException, ConnectionToRemoteDeviceException { + try (Transport transport = createTransport()) { + return new HostDisconnectFromRemoteTcpDevice(transport).disconnect(tcpAddressEntity); + } + } + + public List getDevices() throws IOException, JadbException { + try (Transport transport = createTransport()) { + transport.send("host:devices"); + transport.verifyResponse(); + String body = transport.readString(); + return parseDevices(body); + } + } + + public DeviceWatcher createDeviceWatcher(DeviceDetectionListener listener) throws IOException, JadbException { + Transport transport = createTransport(); + transport.send("host:track-devices"); + transport.verifyResponse(); + return new DeviceWatcher(transport, listener, this); + } + + public List parseDevices(String body) { + String[] lines = body.split("\n"); + ArrayList devices = new ArrayList<>(lines.length); + for (String line : lines) { + String[] parts = line.split("\t"); + if (parts.length > 1) { + devices.add(new JadbDevice(parts[0], this)); // parts[1] is type + } + } + return devices; + } + + public JadbDevice getAnyDevice() { + return JadbDevice.createAny(this); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java new file mode 100644 index 0000000..a03bb69 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbDevice.java @@ -0,0 +1,256 @@ +package com.xypower.wpywapp.jadb; + + + +import com.xypower.wpywapp.jadb.managers.Bash; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class JadbDevice { + @SuppressWarnings("squid:S00115") + public enum State { + Unknown, + Offline, + Device, + Recovery, + BootLoader, + Unauthorized, + Authorizing, + Sideload, + Connecting, + Rescue + } + + //noinspection OctalInteger + private static final int DEFAULT_MODE = 0664; + private final String serial; + private final ITransportFactory transportFactory; + private static final int DEFAULT_TCPIP_PORT = 5555; + + JadbDevice(String serial, ITransportFactory tFactory) { + this.serial = serial; + this.transportFactory = tFactory; + } + + static JadbDevice createAny(JadbConnection connection) { + return new JadbDevice(connection); + } + + private JadbDevice(ITransportFactory tFactory) { + serial = null; + this.transportFactory = tFactory; + } + + private State convertState(String type) { + switch (type) { + case "device": return State.Device; + case "offline": return State.Offline; + case "bootloader": return State.BootLoader; + case "recovery": return State.Recovery; + case "unauthorized": return State.Unauthorized; + case "authorizing" : return State.Authorizing; + case "connecting": return State.Connecting; + case "sideload": return State.Sideload; + case "rescue" : return State.Rescue; + default: return State.Unknown; + } + } + + private Transport getTransport() throws IOException, JadbException { + Transport transport = transportFactory.createTransport(); + // Do not use try-with-resources here. We want to return unclosed Transport and it is up to caller + // to close it. Here we close it only in case of exception. + try { + send(transport, serial == null ? "host:transport-any" : "host:transport:" + serial ); + } catch (IOException|JadbException e) { + transport.close(); + throw e; + } + return transport; + } + + public String getSerial() { + return serial; + } + + public State getState() throws IOException, JadbException { + try (Transport transport = transportFactory.createTransport()) { + send(transport, serial == null ? "host:get-state" : "host-serial:" + serial + ":get-state"); + return convertState(transport.readString()); + } + } + + /**

Execute a shell command.

+ * + *

For Lollipop and later see: {@link #execute(String, String...)}

+ * + * @param command main command to run. E.g. "ls" + * @param args arguments to the command. + * @return combined stdout/stderr stream. + * @throws IOException + * @throws JadbException + */ + public InputStream executeShell(String command, String... args) throws IOException, JadbException { + Transport transport = getTransport(); + StringBuilder shellLine = buildCmdLine(command, args); + send(transport, "shell:" + shellLine.toString()); + return new AdbFilterInputStream(new BufferedInputStream(transport.getInputStream())); + } + + /** + * + * @deprecated Use InputStream executeShell(String command, String... args) method instead. Together with + * Stream.copy(in, out), it is possible to achieve the same effect. + */ + @Deprecated + public void executeShell(OutputStream output, String command, String... args) throws IOException, JadbException { + try (Transport transport = getTransport()) { + StringBuilder shellLine = buildCmdLine(command, args); + send(transport, "shell:" + shellLine.toString()); + if (output == null) + return; + + AdbFilterOutputStream out = new AdbFilterOutputStream(output); + transport.readResponseTo(out); + } + } + + /**

Execute a command with raw binary output.

+ * + *

Support for this command was added in Lollipop (Android 5.0), and is the recommended way to transmit binary + * data with that version or later. For earlier versions of Android, use + * {@link #executeShell(String, String...)}.

+ * + * @param command main command to run, e.g. "screencap" + * @param args arguments to the command, e.g. "-p". + * @return combined stdout/stderr stream. + * @throws IOException + * @throws JadbException + */ + public InputStream execute(String command, String... args) throws IOException, JadbException { + Transport transport = getTransport(); + StringBuilder shellLine = buildCmdLine(command, args); + send(transport, "exec:" + shellLine.toString()); + return new BufferedInputStream(transport.getInputStream()); + } + + /** + * Builds a command line string from the command and its arguments. + * + * @param command the command. + * @param args the list of arguments. + * @return the command line. + */ + private StringBuilder buildCmdLine(String command, String... args) { + StringBuilder shellLine = new StringBuilder(command); + for (String arg : args) { + shellLine.append(" "); + shellLine.append(Bash.quote(arg)); + } + return shellLine; + } + + /** + * Enable tcpip on the default port (5555) + * + * @return success or failure + */ + public void enableAdbOverTCP() throws IOException, JadbException { + enableAdbOverTCP(DEFAULT_TCPIP_PORT); + } + + /** + * Enable tcpip on a specific port + * + * @param port for the device to bind on + * + * @return success or failure + */ + public void enableAdbOverTCP(int port) throws IOException, JadbException { + try (Transport transport = getTransport()) { + send(transport, String.format("tcpip:%d", port)); + } + } + + public List list(String remotePath) throws IOException, JadbException { + try (Transport transport = getTransport()) { + SyncTransport sync = transport.startSync(); + sync.send("LIST", remotePath); + + List result = new ArrayList<>(); + for (RemoteFileRecord dent = sync.readDirectoryEntry(); dent != RemoteFileRecord.DONE; dent = sync.readDirectoryEntry()) { + result.add(dent); + } + return result; + } + } + + public void push(InputStream source, long lastModified, int mode, RemoteFile remote) throws IOException, JadbException { + try (Transport transport = getTransport()) { + SyncTransport sync = transport.startSync(); + sync.send("SEND", remote.getPath() + "," + mode); + + sync.sendStream(source); + + sync.sendStatus("DONE", (int) lastModified); + sync.verifyStatus(); + } + } + + public void push(File local, RemoteFile remote) throws IOException, JadbException { + try (FileInputStream fileStream = new FileInputStream(local)) { + push(fileStream, TimeUnit.MILLISECONDS.toSeconds(local.lastModified()), DEFAULT_MODE, remote); + } + } + + public void pull(RemoteFile remote, OutputStream destination) throws IOException, JadbException { + try (Transport transport = getTransport()) { + SyncTransport sync = transport.startSync(); + sync.send("RECV", remote.getPath()); + + sync.readChunksTo(destination); + } + } + + public void pull(RemoteFile remote, File local) throws IOException, JadbException { + try (FileOutputStream fileStream = new FileOutputStream(local)) { + pull(remote, fileStream); + } + } + + private void send(Transport transport, String command) throws IOException, JadbException { + transport.send(command); + transport.verifyResponse(); + } + + @Override + public String toString() { + return "Android Device with serial " + serial; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((serial == null) ? 0 : serial.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + JadbDevice other = (JadbDevice) obj; + if (serial == null) { + return other.serial == null; + } + return serial.equals(other.serial); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java b/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java new file mode 100644 index 0000000..068612a --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/JadbException.java @@ -0,0 +1,10 @@ +package com.xypower.wpywapp.jadb; + +public class JadbException extends Exception { + + public JadbException(String message) { + super(message); + } + + private static final long serialVersionUID = -3879283786835654165L; +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java b/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java new file mode 100644 index 0000000..9f39fa8 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/LookBackFilteringOutputStream.java @@ -0,0 +1,45 @@ +package com.xypower.wpywapp.jadb; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayDeque; + +public class LookBackFilteringOutputStream extends FilterOutputStream { + private final ArrayDeque buffer; + private final int lookBackBufferSize; + + protected LookBackFilteringOutputStream(OutputStream inner, int lookBackBufferSize) + { + super(inner); + this.lookBackBufferSize = lookBackBufferSize; + this.buffer = new ArrayDeque<>(lookBackBufferSize); + } + + protected void unwrite() { + buffer.removeFirst(); + } + + protected ArrayDeque lookback() { + return buffer; + } + + @Override + public void write(int c) throws IOException { + buffer.addLast((byte) c); + flushBuffer(lookBackBufferSize); + } + + @Override + public void flush() throws IOException { + flushBuffer(0); + out.flush(); + } + + private void flushBuffer(int size) throws IOException { + while (buffer.size() > size) { + Byte b = buffer.removeFirst(); + out.write(b); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java new file mode 100644 index 0000000..dfb2313 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFile.java @@ -0,0 +1,31 @@ +package com.xypower.wpywapp.jadb; + +/** + * Created by vidstige on 2014-03-20 + */ +public class RemoteFile { + private final String path; + + public RemoteFile(String path) { this.path = path; } + + public String getName() { throw new UnsupportedOperationException(); } + public int getSize() { throw new UnsupportedOperationException(); } + public int getLastModified() { throw new UnsupportedOperationException(); } + public boolean isDirectory() { throw new UnsupportedOperationException(); } + + public String getPath() { return path;} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RemoteFile that = (RemoteFile) o; + return path.equals(that.path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java new file mode 100644 index 0000000..6775790 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/RemoteFileRecord.java @@ -0,0 +1,34 @@ +package com.xypower.wpywapp.jadb; + +/** + * Created by vidstige on 2014-03-19. + */ +class RemoteFileRecord extends RemoteFile { + public static final RemoteFileRecord DONE = new RemoteFileRecord(null, 0, 0, 0); + + private final int mode; + private final int size; + private final int lastModified; + + public RemoteFileRecord(String name, int mode, int size, int lastModified) { + super(name); + this.mode = mode; + this.size = size; + this.lastModified = lastModified; + } + + @Override + public int getSize() { + return size; + } + + @Override + public int getLastModified() { + return lastModified; + } + + @Override + public boolean isDirectory() { + return (mode & (1 << 14)) == (1 << 14); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java b/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java new file mode 100644 index 0000000..7a91d2e --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/Stream.java @@ -0,0 +1,27 @@ +package com.xypower.wpywapp.jadb; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; + +public class Stream { + private Stream() { + throw new IllegalStateException("Utility class"); + } + + public static void copy(InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[1024 * 10]; + int len; + while ((len = in.read(buffer)) != -1) { + out.write(buffer, 0, len); + } + } + + public static String readAll(InputStream input, Charset charset) throws IOException { + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + Stream.copy(input, tmp); + return new String(tmp.toByteArray(), charset); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java b/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java new file mode 100644 index 0000000..9b52b55 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/Subprocess.java @@ -0,0 +1,9 @@ +package com.xypower.wpywapp.jadb; + +import java.io.IOException; + +public class Subprocess { + public Process execute(String[] command) throws IOException { + return Runtime.getRuntime().exec(command); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java b/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java new file mode 100644 index 0000000..f0e5186 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/SyncTransport.java @@ -0,0 +1,115 @@ +package com.xypower.wpywapp.jadb; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * Created by vidstige on 2014-03-19. + */ +public class SyncTransport { + + private final DataOutput output; + private final DataInput input; + + public SyncTransport(DataOutput outputStream, DataInput inputStream) { + output = outputStream; + input = inputStream; + } + + public void send(String syncCommand, String name) throws IOException { + if (syncCommand.length() != 4) throw new IllegalArgumentException("sync commands must have length 4"); + output.writeBytes(syncCommand); + byte[] data = name.getBytes(StandardCharsets.UTF_8); + output.writeInt(Integer.reverseBytes(data.length)); + output.write(data); + } + + public void sendStatus(String statusCode, int length) throws IOException { + output.writeBytes(statusCode); + output.writeInt(Integer.reverseBytes(length)); + } + + public void verifyStatus() throws IOException, JadbException { + String status = readString(4); + int length = readInt(); + if ("FAIL".equals(status)) { + String error = readString(length); + throw new JadbException(error); + } + if (!"OKAY".equals(status)) { + throw new JadbException("Unknown error: " + status); + } + } + + private int readInt() throws IOException { + return Integer.reverseBytes(input.readInt()); + } + + private String readString(int length) throws IOException { + byte[] buffer = new byte[length]; + input.readFully(buffer); + return new String(buffer, StandardCharsets.UTF_8); + } + + public void sendDirectoryEntry(RemoteFile file) throws IOException { + output.writeBytes("DENT"); + output.writeInt(Integer.reverseBytes(0666 | (file.isDirectory() ? (1 << 14) : 0))); + output.writeInt(Integer.reverseBytes(file.getSize())); + output.writeInt(Integer.reverseBytes(file.getLastModified())); + byte[] pathChars = file.getPath().getBytes(StandardCharsets.UTF_8); + output.writeInt(Integer.reverseBytes(pathChars.length)); + output.write(pathChars); + } + + public void sendDirectoryEntryDone() throws IOException { + output.writeBytes("DONE"); + output.writeBytes("\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); // equivalent to the length of a "normal" dent + } + + public RemoteFileRecord readDirectoryEntry() throws IOException { + String id = readString(4); + int mode = readInt(); + int size = readInt(); + int time = readInt(); + int nameLength = readInt(); + String name = readString(nameLength); + + if (!"DENT".equals(id)) return RemoteFileRecord.DONE; + return new RemoteFileRecord(name, mode, size, time); + } + + private void sendChunk(byte[] buffer, int offset, int length) throws IOException { + output.writeBytes("DATA"); + output.writeInt(Integer.reverseBytes(length)); + output.write(buffer, offset, length); + } + + private int readChunk(byte[] buffer) throws IOException, JadbException { + String id = readString(4); + int n = readInt(); + if ("FAIL".equals(id)) { + throw new JadbException(readString(n)); + } + if (!"DATA".equals(id)) return -1; + input.readFully(buffer, 0, n); + return n; + } + + public void sendStream(InputStream in) throws IOException { + byte[] buffer = new byte[1024 * 64]; + int n = in.read(buffer); + while (n != -1) { + sendChunk(buffer, 0, n); + n = in.read(buffer); + } + } + + public void readChunksTo(OutputStream stream) throws IOException, JadbException { + byte[] buffer = new byte[1024 * 64]; + int n = readChunk(buffer); + while (n != -1) { + stream.write(buffer, 0, n); + n = readChunk(buffer); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java b/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java new file mode 100644 index 0000000..52b97b4 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/Transport.java @@ -0,0 +1,75 @@ +package com.xypower.wpywapp.jadb; + +import java.io.*; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +class Transport implements Closeable { + + private final OutputStream outputStream; + private final InputStream inputStream; + private final DataInputStream dataInput; + private final DataOutputStream dataOutput; + + private Transport(OutputStream outputStream, InputStream inputStream) { + this.outputStream = outputStream; + this.inputStream = inputStream; + this.dataInput = new DataInputStream(inputStream); + this.dataOutput = new DataOutputStream(outputStream); + } + + public Transport(Socket socket) throws IOException { + this(socket.getOutputStream(), socket.getInputStream()); + } + + public String readString() throws IOException { + String encodedLength = readString(4); + int length = Integer.parseInt(encodedLength, 16); + return readString(length); + } + + public void readResponseTo(OutputStream output) throws IOException { + Stream.copy(inputStream, output); + } + + public InputStream getInputStream() { + return inputStream; + } + + public void verifyResponse() throws IOException, JadbException { + String response = readString(4); + if (!"OKAY".equals(response)) { + String error = readString(); + throw new JadbException("command failed: " + error); + } + } + + public String readString(int length) throws IOException { + byte[] responseBuffer = new byte[length]; + dataInput.readFully(responseBuffer); + return new String(responseBuffer, StandardCharsets.UTF_8); + } + + private String getCommandLength(String command) { + return String.format("%04x", command.getBytes().length); + } + + public void send(String command) throws IOException { + OutputStreamWriter writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8); + writer.write(getCommandLength(command)); + writer.write(command); + writer.flush(); + } + + public SyncTransport startSync() throws IOException, JadbException { + send("sync:"); + verifyResponse(); + return new SyncTransport(dataOutput, dataInput); + } + + @Override + public void close() throws IOException { + dataInput.close(); + dataOutput.close(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java new file mode 100644 index 0000000..8ecab80 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Bash.java @@ -0,0 +1,11 @@ +package com.xypower.wpywapp.jadb.managers; + +public class Bash { + private Bash() { + throw new IllegalStateException("Utility class"); + } + + public static String quote(String s) { + return "'" + s.replace("'", "'\\''") + "'"; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java new file mode 100644 index 0000000..8a1696d --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/Package.java @@ -0,0 +1,26 @@ +package com.xypower.wpywapp.jadb.managers; + +/** + * Android package + */ +public class Package { + private final String name; + + public Package(String name) { + this.name = name; + } + + @Override + public String toString() { return name; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Package)) return false; + Package that = (Package) o; + return name.equals(that.name); + } + + @Override + public int hashCode() { return name.hashCode(); } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java new file mode 100644 index 0000000..b5c7aa3 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PackageManager.java @@ -0,0 +1,144 @@ +package com.xypower.wpywapp.jadb.managers; + + + +import com.xypower.wpywapp.jadb.JadbDevice; +import com.xypower.wpywapp.jadb.JadbException; +import com.xypower.wpywapp.jadb.RemoteFile; +import com.xypower.wpywapp.jadb.Stream; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Java interface to package manager. Launches package manager through jadb + */ +public class PackageManager { + private final JadbDevice device; + + public PackageManager(JadbDevice device) { + this.device = device; + } + + public List getPackages() throws IOException, JadbException { + try (BufferedReader input = new BufferedReader(new InputStreamReader(device.executeShell("pm", "list", "packages"), StandardCharsets.UTF_8))) { + ArrayList result = new ArrayList<>(); + String line; + while ((line = input.readLine()) != null) { + final String prefix = "package:"; + if (line.startsWith(prefix)) { + result.add(new Package(line.substring(prefix.length()))); + } + } + return result; + } + } + + private String getErrorMessage(String operation, String target, String errorMessage) { + return "Could not " + operation + " " + target + ": " + errorMessage; + } + + private void verifyOperation(String operation, String target, String result) throws JadbException { + if (!result.contains("Success")) throw new JadbException(getErrorMessage(operation, target, result)); + } + + private void remove(RemoteFile file) throws IOException, JadbException { + InputStream s = device.executeShell("rm", "-f", file.getPath()); + Stream.readAll(s, StandardCharsets.UTF_8); + } + + private void install(File apkFile, List extraArguments) throws IOException, JadbException { + RemoteFile remote = new RemoteFile("/data/local/tmp/" + apkFile.getName()); + device.push(apkFile, remote); + List arguments = new ArrayList<>(); + arguments.add("install"); + arguments.addAll(extraArguments); + arguments.add(remote.getPath()); + InputStream s = device.executeShell("pm", arguments.toArray(new String[0])); + String result = Stream.readAll(s, StandardCharsets.UTF_8); + remove(remote); + verifyOperation("install", apkFile.getName(), result); + } + + public void install(File apkFile) throws IOException, JadbException { + install(apkFile, new ArrayList(0)); + } + + public void installWithOptions(File apkFile, List options) throws IOException, JadbException { + List optionsAsStr = new ArrayList<>(options.size()); + + for(InstallOption installOption : options) { + optionsAsStr.add(installOption.getStringRepresentation()); + } + install(apkFile, optionsAsStr); + } + + public void forceInstall(File apkFile) throws IOException, JadbException { + installWithOptions(apkFile, Collections.singletonList(REINSTALL_KEEPING_DATA)); + } + + public void uninstall(java.lang.Package name) throws IOException, JadbException { + InputStream s = device.executeShell("pm", "uninstall", name.toString()); + String result = Stream.readAll(s, StandardCharsets.UTF_8); + verifyOperation("uninstall", name.toString(), result); + } + + public void launch(java.lang.Package name) throws IOException, JadbException { + InputStream s = device.executeShell("monkey", "-p", name.toString(), "-c", "android.intent.category.LAUNCHER", "1"); + s.close(); + } + + // + public static class InstallOption { + private final StringBuilder stringBuilder = new StringBuilder(); + + InstallOption(String ... varargs) { + String suffix = ""; + for(String str: varargs) { + stringBuilder.append(suffix).append(str); + suffix = " "; + } + } + + private String getStringRepresentation() { + return stringBuilder.toString(); + } + } + + public static final InstallOption WITH_FORWARD_LOCK = new InstallOption("-l"); + + public static final InstallOption REINSTALL_KEEPING_DATA = + new InstallOption("-r"); + + public static final InstallOption ALLOW_TEST_APK = + new InstallOption("-t"); + + @SuppressWarnings("squid:S00100") + public static InstallOption WITH_INSTALLER_PACKAGE_NAME(String name) + { + return new InstallOption("-t", name); + } + + @SuppressWarnings("squid:S00100") + public static InstallOption ON_SHARED_MASS_STORAGE(String name) { + return new InstallOption("-s", name); + } + + @SuppressWarnings("squid:S00100") + public static InstallOption ON_INTERNAL_SYSTEM_MEMORY(String name) { + return new InstallOption("-f", name); + } + + public static final InstallOption ALLOW_VERSION_DOWNGRADE = + new InstallOption("-d"); + + /** + * This option is supported only from Android 6.X+ + */ + public static final InstallOption GRANT_ALL_PERMISSIONS = new InstallOption("-g"); + + // +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java new file mode 100644 index 0000000..8767bd0 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/managers/PropertyManager.java @@ -0,0 +1,57 @@ +package com.xypower.wpywapp.jadb.managers; + + + +import com.xypower.wpywapp.jadb.JadbDevice; +import com.xypower.wpywapp.jadb.JadbException; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A class which works with properties, uses getprop and setprop methods of android shell + */ +public class PropertyManager { + private final Pattern pattern = Pattern.compile("^\\[([a-zA-Z0-9_.-]*)]:.\\[([^\\[\\]]*)]"); + private final JadbDevice device; + + public PropertyManager(JadbDevice device) { + this.device = device; + } + + public Map getprop() throws IOException, JadbException { + try (BufferedReader bufferedReader = + new BufferedReader(new InputStreamReader(device.executeShell("getprop"), StandardCharsets.UTF_8))) { + return parseProp(bufferedReader); + } + } + + private Map parseProp(BufferedReader bufferedReader) throws IOException { + HashMap result = new HashMap<>(); + + String line; + Matcher matcher = pattern.matcher(""); + + while ((line = bufferedReader.readLine()) != null) { + matcher.reset(line); + + if (matcher.find()) { + if (matcher.groupCount() < 2) { + System.err.println("Property line: " + line + " does not match pattern. Ignoring"); + continue; + } + String key = matcher.group(1); + String value = matcher.group(2); + result.put(key, value); + } + } + + return result; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java new file mode 100644 index 0000000..592cb1c --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbDeviceResponder.java @@ -0,0 +1,25 @@ +package com.xypower.wpywapp.jadb.server; + + + +import com.xypower.wpywapp.jadb.JadbException; +import com.xypower.wpywapp.jadb.RemoteFile; + +import java.io.*; +import java.util.List; + +/** + * Created by vidstige on 20/03/14. + */ +public interface AdbDeviceResponder { + String getSerial(); + String getType(); + + void filePushed(RemoteFile path, int mode, ByteArrayOutputStream buffer) throws JadbException; + void filePulled(RemoteFile path, ByteArrayOutputStream buffer) throws JadbException, IOException; + + void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException; + void enableIpCommand(String ip, DataOutputStream outputStream) throws IOException; + + List list(String path) throws IOException; +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java new file mode 100644 index 0000000..631eae5 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbProtocolHandler.java @@ -0,0 +1,248 @@ +package com.xypower.wpywapp.jadb.server; + + + +import com.xypower.wpywapp.jadb.JadbException; +import com.xypower.wpywapp.jadb.RemoteFile; +import com.xypower.wpywapp.jadb.SyncTransport; + +import java.io.*; +import java.net.ProtocolException; +import java.net.Socket; +import java.nio.charset.StandardCharsets; + +class AdbProtocolHandler implements Runnable { + private final Socket socket; + private final AdbResponder responder; + private AdbDeviceResponder selected; + + public AdbProtocolHandler(Socket socket, AdbResponder responder) { + this.socket = socket; + this.responder = responder; + } + + private AdbDeviceResponder findDevice(String serial) throws ProtocolException { + for (AdbDeviceResponder d : responder.getDevices()) { + if (d.getSerial().equals(serial)) return d; + } + throw new ProtocolException("'" + serial + "' not connected"); + } + + @Override + public void run() { + try { + runServer(); + } catch (IOException e) { + if (e.getMessage() != null) // thrown when exiting for some reason + System.out.println("IO Error: " + e.getMessage()); + } + } + + private void runServer() throws IOException { + try ( + DataInputStream input = new DataInputStream(socket.getInputStream()); + DataOutputStream output = new DataOutputStream(socket.getOutputStream()) + ) { + //noinspection StatementWithEmptyBody + while (processCommand(input, output)) { + // nothing to do here + } + } + } + + private boolean processCommand(DataInput input, DataOutputStream output) throws IOException { + String command = readCommand(input); + responder.onCommand(command); + + try { + if ("host:version".equals(command)) { + hostVersion(output); + } else if ("host:transport-any".equals(command)) { + hostTransportAny(output); + } else if ("host:devices".equals(command)) { + hostDevices(output); + } else if (command.startsWith("host:transport:")) { + hostTransport(output, command); + } else if ("sync:".equals(command)) { + sync(output, input); + } else if (command.startsWith("shell:")) { + shell(input, output, command); + return false; + } else if ("host:get-state".equals(command)) { + hostGetState(output); + } else if (command.startsWith("host-serial:")) { + hostSerial(output, command); + } else if (command.startsWith("tcpip:")) { + handleTcpip(output, command); + } else { + throw new ProtocolException("Unknown command: " + command); + } + } catch (ProtocolException e) { + output.writeBytes("FAIL"); + send(output, e.getMessage()); + } + output.flush(); + return true; + } + + private void handleTcpip(DataOutputStream output, String command) throws IOException { + output.writeBytes("OKAY"); + selected.enableIpCommand(command.substring("tcpip:".length()), output); + } + + private void hostSerial(DataOutput output, String command) throws IOException { + String[] strs = command.split(":",0); + if (strs.length != 3) { + throw new ProtocolException("Invalid command: " + command); + } + + String serial = strs[1]; + boolean found = false; + output.writeBytes("OKAY"); + for (AdbDeviceResponder d : responder.getDevices()) { + if (d.getSerial().equals(serial)) { + send(output, d.getType()); + found = true; + break; + } + } + + if (!found) { + send(output, "unknown"); + } + } + + private void hostGetState(DataOutput output) throws IOException { + // TODO: Check so that exactly one device is selected. + AdbDeviceResponder device = responder.getDevices().get(0); + output.writeBytes("OKAY"); + send(output, device.getType()); + } + + private void shell(DataInput input, DataOutputStream output, String command) throws IOException { + String shellCommand = command.substring("shell:".length()); + output.writeBytes("OKAY"); + shell(shellCommand, output, input); + } + + private void hostTransport(DataOutput output, String command) throws IOException { + String serial = command.substring("host:transport:".length()); + selected = findDevice(serial); + output.writeBytes("OKAY"); + } + + private void hostDevices(DataOutput output) throws IOException { + ByteArrayOutputStream tmp = new ByteArrayOutputStream(); + DataOutputStream writer = new DataOutputStream(tmp); + for (AdbDeviceResponder d : responder.getDevices()) { + writer.writeBytes(d.getSerial() + "\t" + d.getType() + "\n"); + } + output.writeBytes("OKAY"); + send(output, new String(tmp.toByteArray(), StandardCharsets.UTF_8)); + } + + private void hostTransportAny(DataOutput output) throws IOException { + // TODO: Check so that exactly one device is selected. + selected = responder.getDevices().get(0); + output.writeBytes("OKAY"); + } + + private void hostVersion(DataOutput output) throws IOException { + output.writeBytes("OKAY"); + send(output, String.format("%04x", responder.getVersion())); + } + + private void shell(String command, DataOutputStream stdout, DataInput stdin) throws IOException { + selected.shell(command, stdout, stdin); + } + + private int readInt(DataInput input) throws IOException { + return Integer.reverseBytes(input.readInt()); + } + + private int readHexInt(DataInput input) throws IOException { + return Integer.parseInt(readString(input, 4), 16); + } + + private String readString(DataInput input, int length) throws IOException { + byte[] responseBuffer = new byte[length]; + input.readFully(responseBuffer); + return new String(responseBuffer, StandardCharsets.UTF_8); + } + + private String readCommand(DataInput input) throws IOException { + int length = readHexInt(input); + return readString(input, length); + } + + private void sync(DataOutput output, DataInput input) throws IOException { + output.writeBytes("OKAY"); + try { + String id = readString(input, 4); + int length = readInt(input); + switch (id) { + case "SEND": + syncSend(output, input, length); + break; + case "RECV": + syncRecv(output, input, length); + break; + case "LIST": + syncList(output, input, length); + break; + default: + throw new JadbException("Unknown sync id " + id); + } + } catch (JadbException e) { // sync response with a different type of fail message + SyncTransport sync = getSyncTransport(output, input); + sync.send("FAIL", e.getMessage()); + } + } + + private void syncRecv(DataOutput output, DataInput input, int length) throws IOException, JadbException { + String remotePath = readString(input, length); + SyncTransport transport = getSyncTransport(output, input); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + selected.filePulled(new RemoteFile(remotePath), buffer); + transport.sendStream(new ByteArrayInputStream(buffer.toByteArray())); + transport.sendStatus("DONE", 0); // ignored + } + + private void syncSend(DataOutput output, DataInput input, int length) throws IOException, JadbException { + String remotePath = readString(input, length); + int idx = remotePath.lastIndexOf(','); + String path = remotePath; + int mode = 0666; + if (idx > 0) { + path = remotePath.substring(0, idx); + mode = Integer.parseInt(remotePath.substring(idx + 1)); + } + SyncTransport transport = getSyncTransport(output, input); + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + transport.readChunksTo(buffer); + selected.filePushed(new RemoteFile(path), mode, buffer); + transport.sendStatus("OKAY", 0); // 0 = ignored + } + + private void syncList(DataOutput output, DataInput input, int length) throws IOException, JadbException { + String remotePath = readString(input, length); + SyncTransport transport = getSyncTransport(output, input); + for (RemoteFile file : selected.list(remotePath)) { + transport.sendDirectoryEntry(file); + } + transport.sendDirectoryEntryDone(); + } + + private String getCommandLength(String command) { + return String.format("%04x", command.length()); + } + + private void send(DataOutput writer, String response) throws IOException { + writer.writeBytes(getCommandLength(response)); + writer.writeBytes(response); + } + + private SyncTransport getSyncTransport(DataOutput output, DataInput input) { + return new SyncTransport(output, input); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java new file mode 100644 index 0000000..1956843 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbResponder.java @@ -0,0 +1,14 @@ +package com.xypower.wpywapp.jadb.server; + +import java.util.List; + +/** + * Created by vidstige on 20/03/14. + */ +public interface AdbResponder { + void onCommand(String command); + + int getVersion(); + + List getDevices(); +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java new file mode 100644 index 0000000..13beae8 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/AdbServer.java @@ -0,0 +1,26 @@ +package com.xypower.wpywapp.jadb.server; + +import java.net.Socket; + +/** + * Created by vidstige on 2014-03-20 + */ +public class AdbServer extends SocketServer { + + public static final int DEFAULT_PORT = 15037; + private final AdbResponder responder; + + public AdbServer(AdbResponder responder) { + this(responder, DEFAULT_PORT); + } + + public AdbServer(AdbResponder responder, int port) { + super(port); + this.responder = responder; + } + + @Override + public Runnable createResponder(Socket socket) { + return new AdbProtocolHandler(socket, responder); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java b/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java new file mode 100644 index 0000000..c4a24e9 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/jadb/server/SocketServer.java @@ -0,0 +1,71 @@ +package com.xypower.wpywapp.jadb.server; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +// >set ANDROID_ADB_SERVER_PORT=15037 +public abstract class SocketServer implements Runnable { + + private final int port; + private ServerSocket socket; + private Thread thread; + + private boolean isStarted = false; + private final Object lockObject = new Object(); + + protected SocketServer(int port) { + this.port = port; + } + + public void start() throws InterruptedException { + thread = new Thread(this, "Fake Adb Server"); + thread.setDaemon(true); + thread.start(); + waitForServer(); + } + + public int getPort() { + return port; + } + + @SuppressWarnings("squid:S2189") // server is stopped by closing SocketServer + @Override + public void run() { + try { + socket = new ServerSocket(port); + socket.setReuseAddress(true); + + serverReady(); + + while (true) { + Socket c = socket.accept(); + createResponder(c).run(); + } + } catch (IOException e) { + // Empty on purpose + } + } + + private void serverReady() { + synchronized (lockObject) { + isStarted = true; + lockObject.notifyAll(); + } + } + + private void waitForServer() throws InterruptedException { + synchronized (lockObject) { + while (!isStarted) { + lockObject.wait(); + } + } + } + + protected abstract Runnable createResponder(Socket socket); + + public void stop() throws IOException, InterruptedException { + socket.close(); + thread.join(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore b/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle b/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle new file mode 100644 index 0000000..4fd917d --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/build.gradle @@ -0,0 +1,60 @@ +package com.xypower.wpywapp.libadb +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +plugins { + id 'com.android.library' + id 'maven-publish' +} + +group = 'io.github.muntashirakon' +version = '3.0.0' + +android { + compileSdk 34 + namespace "io.github.muntashirakon.adb" + + defaultConfig { + minSdk 1 + targetSdk 34 + aarMetadata { + minCompileSdk = 1 + } + } + + buildTypes { + release { + minifyEnabled false + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } +} + +publishing { + publications { + release(MavenPublication) { + artifactId = 'libadb' + afterEvaluate { + from components.release + } + } + } +} + +dependencies { + implementation "androidx.annotation:annotation:1.7.1" + implementation 'org.bouncycastle:bcprov-jdk15to18:1.78' + implementation 'com.github.MuntashirAkon.spake2-java:android:2.0.0' + + testImplementation 'junit:junit:4.13.2' +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c9d05a3 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java new file mode 100644 index 0000000..d35030a --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java @@ -0,0 +1,509 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.content.Context; +import android.os.Build; + +import androidx.annotation.CallSuper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.WorkerThread; + +import com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android.AdbMdns; + +import java.io.Closeable; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import javax.security.auth.DestroyFailedException; + + +@SuppressWarnings("unused") +public abstract class AbsAdbConnectionManager implements Closeable { + private final Object mLock = new Object(); + @Nullable + private AdbConnection mAdbConnection; + private String mHostAddress = "127.0.0.1"; + private int mApi = Build.VERSION_CODES.P; + private long mTimeout = Long.MAX_VALUE; + private TimeUnit mTimeoutUnit = TimeUnit.MILLISECONDS; + private boolean mThrowOnUnauthorised = false; + + /** + * Return generated/stored private key. + */ + @NonNull + protected abstract PrivateKey getPrivateKey(); + + /** + * Return public key wrapped around a certificate. + */ + @NonNull + protected abstract Certificate getCertificate(); + + /** + * Return a name for the device. This can be the app label, hostname or user@hostname. + */ + @NonNull + protected abstract String getDeviceName(); + + /** + * Set host address for this connection. On the same device, this should be {@code 127.0.0.1}. + */ + @CallSuper + public void setHostAddress(@NonNull String hostAddress) { + mHostAddress = Objects.requireNonNull(hostAddress); + } + + /** + * Get host address for this connection. Default value is {@code 127.0.0.1}. + */ + @NonNull + public String getHostAddress() { + return mHostAddress; + } + + /** + * Set Android API (i.e. SDK) version for this connection. If the daemon and the client are located in the same + * directory, the value should be {@link Build.VERSION#SDK_INT} in order to improve performance as well as security. + * + * @param api The API version, default is {@link Build.VERSION_CODES#BASE}. + */ + public void setApi(int api) { + this.mApi = api; + } + + /** + * Get Android API (i.e. SDK) version for this connection. Default value is {@link Build.VERSION_CODES#BASE}. + */ + public int getApi() { + return mApi; + } + + /** + * Set time to wait for the connection to be made. + * + * @param timeout Timeout value + * @param unit Timeout unit + */ + @CallSuper + public void setTimeout(long timeout, TimeUnit unit) { + mTimeout = timeout; + mTimeoutUnit = unit; + } + + /** + * Get time to wait for the connection to be made. If not set using {@link #setTimeout(long, TimeUnit)}, the default + * timeout is {@link Long#MAX_VALUE} milliseconds. + * + * @return Timeout in milliseconds + */ + public long getTimeout() { + return mTimeoutUnit.toMillis(mTimeout); + } + + /** + * Get the unit for the timeout. If not set using {@link #setTimeout(long, TimeUnit)}, the default timeout unit is + * {@link TimeUnit#MILLISECONDS}. + */ + @NonNull + public TimeUnit getTimeoutUnit() { + return mTimeoutUnit; + } + + /** + * Set whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication + * attempt. + * + * @param throwOnUnauthorised {@code true} to throw {@link AdbAuthenticationFailedException} or {@code false} + * otherwise. + */ + @CallSuper + public void setThrowOnUnauthorised(boolean throwOnUnauthorised) { + mThrowOnUnauthorised = throwOnUnauthorised; + } + + /** + * Get whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication + * attempt. + * + * @return {@code true} if the system is configured to throw {@link AdbAuthenticationFailedException} or + * {@code false} otherwise. The default value is {@code false}. + */ + public boolean isThrowOnUnauthorised() { + return mThrowOnUnauthorised; + } + + /** + * Get the {@link AdbConnection} backed by this object. + * + * @return Underlying {@link AdbConnection}, or {@code null} if the connection hasn't been made yet. + */ + @CallSuper + @Nullable + public AdbConnection getAdbConnection() { + synchronized (mLock) { + return mAdbConnection; + } + } + + /** + * Check if it is connected to an ADB daemon. + * + * @return {@code true} if connected, {@code false} otherwise. + */ + public boolean isConnected() { + synchronized (mLock) { + return mAdbConnection != null && mAdbConnection.isConnected() && mAdbConnection.isConnectionEstablished(); + } + } + + /** + * Attempt to connect to ADB by performing an automatic network discovery of TLS host and port. Host address set by + * {@link #setHostAddress(String)} is ignored. + * + * @param context Application context + * @param timeoutMillis Amount of time spent in searching for a host and a port. + * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection + * attempt is unsuccessful, or it has already been made. + * @throws IOException If the socket connection could not be made. + * @throws InterruptedException If timeout has reached. + * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB + * daemon has rejected the first authentication attempt, which indicates + * that the daemon has not saved the public key from a previous connection. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @WorkerThread + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean connectTls(@NonNull Context context, long timeoutMillis) + throws IOException, InterruptedException, AdbPairingRequiredException { + return autoConnect(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, timeoutMillis); + } + + /** + * Attempt to connect to ADB by performing an automatic network discovery of TCP host and port. Host address set by + * {@link #setHostAddress(String)} is ignored. + * + * @param context Application context + * @param timeoutMillis Amount of time spent in searching for a host and a port. + * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection + * attempt is unsuccessful, or it has already been made. + * @throws IOException If the socket connection could not be made. + * @throws InterruptedException If timeout has reached. + * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB + * daemon has rejected the first authentication attempt, which indicates + * that the daemon has not saved the public key from a previous connection. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @WorkerThread + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean connectTcp(@NonNull Context context, long timeoutMillis) + throws IOException, InterruptedException, AdbPairingRequiredException { + return autoConnect(context, AdbMdns.SERVICE_TYPE_ADB, timeoutMillis); + } + + /** + * Attempt to connect to ADB by performing an automatic network discovery of host and port. Host address set by + * {@link #setHostAddress(String)} is ignored. + * + * @param context Application context + * @param timeoutMillis Amount of time spent in searching for a host and a port. + * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection + * attempt is unsuccessful, or it has already been made. + * @throws IOException If the socket connection could not be made. + * @throws InterruptedException If timeout has reached. + * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB + * daemon has rejected the first authentication attempt, which indicates + * that the daemon has not saved the public key from a previous connection. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @WorkerThread + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) + public boolean autoConnect(@NonNull Context context, long timeoutMillis) + throws IOException, InterruptedException, AdbPairingRequiredException { + synchronized (mLock) { + AtomicInteger atomicPort = new AtomicInteger(-1); + AtomicReference atomicHostAddress = new AtomicReference<>(null); + CountDownLatch resolveHostAndPort = new CountDownLatch(1); + + AdbMdns adbMdnsTcp = new AdbMdns(context, AdbMdns.SERVICE_TYPE_ADB, (hostAddress, port) -> { + if (hostAddress != null) { + atomicHostAddress.set(hostAddress.getHostAddress()); + atomicPort.set(port); + } + resolveHostAndPort.countDown(); + }); + adbMdnsTcp.start(); + + AdbMdns adbMdnsTls = new AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, (hostAddress, port) -> { + if (hostAddress != null) { + atomicHostAddress.set(hostAddress.getHostAddress()); + atomicPort.set(port); + } + resolveHostAndPort.countDown(); + }); + adbMdnsTls.start(); + + try { + if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) { + throw new InterruptedException("Timed out while trying to find a valid host address and port"); + } + } finally { + adbMdnsTcp.stop(); + adbMdnsTls.stop(); + } + + String host = atomicHostAddress.get(); + int port = atomicPort.get(); + + if (host == null || port == -1) { + throw new IOException("Could not find any valid host address or port"); + } + + mHostAddress = host; + mAdbConnection = new AdbConnection.Builder(host, port) + .setApi(mApi) + .setKeyPair(getAdbKeyPair()) + .setDeviceName(Objects.requireNonNull(getDeviceName())) + .build(); + return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); + } + } + + @WorkerThread + @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) + private boolean autoConnect(@NonNull Context context, @AdbMdns.ServiceType @NonNull String serviceType, long timeoutMillis) + throws IOException, InterruptedException, AdbPairingRequiredException { + synchronized (mLock) { + AtomicInteger atomicPort = new AtomicInteger(-1); + AtomicReference atomicHostAddress = new AtomicReference<>(null); + CountDownLatch resolveHostAndPort = new CountDownLatch(1); + + AdbMdns adbMdns = new AdbMdns(context, serviceType, (hostAddress, port) -> { + if (hostAddress != null) { + atomicHostAddress.set(hostAddress.getHostAddress()); + atomicPort.set(port); + } + resolveHostAndPort.countDown(); + }); + adbMdns.start(); + + try { + if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) { + throw new InterruptedException("Timed out while trying to find a valid host address and port"); + } + } finally { + adbMdns.stop(); + } + + String host = atomicHostAddress.get(); + int port = atomicPort.get(); + + if (host == null || port == -1) { + throw new IOException("Could not find any valid host address or port"); + } + + mHostAddress = host; + mAdbConnection = new AdbConnection.Builder(host, port) + .setApi(mApi) + .setKeyPair(getAdbKeyPair()) + .setDeviceName(Objects.requireNonNull(getDeviceName())) + .build(); + return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); + } + } + + /** + * Attempt to connect to ADB given a port number. Host address is set via {@link #setHostAddress(String)}. + * + * @param port Port number + * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection + * attempt is unsuccessful, or it has already been made. + * @throws IOException If the socket connection could not be made. + * @throws InterruptedException If timeout has reached. + * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB + * daemon has rejected the first authentication attempt, which indicates + * that the daemon has not saved the public key from a previous connection. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @WorkerThread + public boolean connect(int port) throws IOException, InterruptedException, AdbPairingRequiredException { + synchronized (mLock) { + if (isConnected()) { + return false; + } + mAdbConnection = new AdbConnection.Builder(mHostAddress, port) + .setApi(mApi) + .setKeyPair(getAdbKeyPair()) + .setDeviceName(Objects.requireNonNull(getDeviceName())) + .build(); + return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); + } + } + + /** + * Attempt to connect to ADB via a host address and a port number. + * + * @param host Host address to use instead of taking it from the {@link #getHostAddress()} + * @param port Port number + * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection + * attempt is unsuccessful, or it has already been made. + * @throws IOException If the socket connection could not be made. + * @throws InterruptedException If timeout has reached. + * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the + * ADB daemon has rejected the first authentication attempt, which + * indicates that the daemon has not saved the public key from a previous + * connection. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @WorkerThread + public boolean connect(@NonNull String host, int port) + throws IOException, InterruptedException, AdbPairingRequiredException { + synchronized (mLock) { + if (isConnected()) { + return false; + } + mHostAddress = host; + mAdbConnection = new AdbConnection.Builder(host, port) + .setApi(mApi) + .setKeyPair(getAdbKeyPair()) + .setDeviceName(Objects.requireNonNull(getDeviceName())) + .build(); + return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); + } + } + + /** + * Disconnect the underlying {@link AdbConnection}. + * + * @throws IOException If the underlying socket fails to close + */ + public void disconnect() throws IOException { + synchronized (mLock) { + if (mAdbConnection != null) { + mAdbConnection.close(); + mAdbConnection = null; + } + } + } + + /** + * Opens an {@link AdbStream} object corresponding to the specified destination. + * This routine will block until the connection completes. + * + * @param destination The destination to open on the target + * @return {@link AdbStream} object corresponding to the specified destination + * @throws IOException If the steam fails or no connection has been made + * @throws InterruptedException If the stream fails while sending the packet + * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8. + */ + @WorkerThread + @NonNull + public AdbStream openStream(String destination) throws IOException, InterruptedException { + synchronized (mLock) { + if (mAdbConnection != null && mAdbConnection.isConnected()) { + try { + return mAdbConnection.open(destination); + } catch (AdbPairingRequiredException e) { + throw new IllegalStateException(e); + } + } + throw new IOException("Not connected to ADB."); + } + } + + /** + * Opens an {@link AdbStream} object corresponding to the specified destination. + * This routine will block until the connection completes. + * + * @param service The service to open. One of the services under {@link LocalServices.Services}. + * @param args Additional arguments supported by the service (see the corresponding constant to learn more). + * @return AdbStream object corresponding to the specified destination + * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 + * @throws IOException If the stream fails while sending the packet + * @throws InterruptedException If we are unable to wait for the connection to finish + */ + @NonNull + public AdbStream openStream(@LocalServices.Services int service, @NonNull String... args) + throws IOException, InterruptedException { + synchronized (mLock) { + if (mAdbConnection != null && mAdbConnection.isConnected()) { + try { + return mAdbConnection.open(service, args); + } catch (AdbPairingRequiredException e) { + throw new IllegalStateException(e); + } + } + throw new IOException("Not connected to ADB."); + } + } + + /** + * Pair with an ADB daemon given port number and pairing code. + * + * @param port Port number + * @param pairingCode The six-digit pairing code as string + * @return {@code true} if the pairing is successful and {@code false} otherwise. + * @throws Exception If pairing failed for some reason. + */ + @WorkerThread + @RequiresApi(Build.VERSION_CODES.GINGERBREAD) + public boolean pair(int port, @NonNull String pairingCode) throws Exception { + return pair(mHostAddress, port, pairingCode); + } + + /** + * Pair with an ADB daemon given host address, port number and pairing code. + * + * @param host Host address to use instead of taking it from the {@link #getHostAddress()} + * @param port Port number + * @param pairingCode The six-digit pairing code as string + * @return {@code true} if the pairing is successful and {@code false} otherwise. + * @throws Exception If pairing failed for some reason. + */ + @WorkerThread + @RequiresApi(Build.VERSION_CODES.GINGERBREAD) + public boolean pair(@NonNull String host, int port, @NonNull String pairingCode) throws Exception { + synchronized (mLock) { + KeyPair keyPair = getAdbKeyPair(); + try (PairingConnectionCtx pairingClient = new PairingConnectionCtx(Objects.requireNonNull(host), port, + StringCompat.getBytes(Objects.requireNonNull(pairingCode), "UTF-8"), keyPair, getDeviceName())) { + // TODO: 5/12/21 Return true/false instead of only exceptions + pairingClient.start(); + } + return true; + } + } + + /** + * Close the underlying {@link AdbConnection} and destroy the private key. + * + * @throws IOException If socket fails to close. + */ + @Override + public void close() throws IOException { + try { + getPrivateKey().destroy(); + } catch (DestroyFailedException | NoSuchMethodError e) { + e.printStackTrace(); + } + if (mAdbConnection != null) { + mAdbConnection.close(); + mAdbConnection = null; + } + } + + @NonNull + private KeyPair getAdbKeyPair() { + return new KeyPair(Objects.requireNonNull(getPrivateKey()), Objects.requireNonNull(getCertificate())); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java new file mode 100644 index 0000000..e769efc --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +/** + * Thrown when the ADB daemon rejects our initial authentication attempt, which typically means that the peer has not + * previously saved our public key. + */ +// Copyright 2020 Sam Palmer +public class AdbAuthenticationFailedException extends RuntimeException { + public AdbAuthenticationFailedException() { + super("Initial authentication attempt rejected by peer."); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java new file mode 100644 index 0000000..d987b51 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnection.java @@ -0,0 +1,749 @@ +// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.os.Build; +import android.util.Log; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.WorkerThread; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.ConnectException; +import java.net.Socket; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.security.auth.DestroyFailedException; + +/** + * This class represents an ADB connection. + */ +// Copyright 2013 Cameron Gutman +public class AdbConnection implements Closeable { + public static final String TAG = AdbConnection.class.getSimpleName(); + + /** + * The underlying socket that this class uses to communicate with the target device. + */ + @NonNull + private final Socket mSocket; + + @NonNull + private final String mHost; + + private final int mPort; + + private final int mApi; + + /** + * The last allocated local stream ID. The ID chosen for the next stream will be this value + 1. + */ + private int mLastLocalId; + + /** + * The input stream that this class uses to read from the socket. + */ + @GuardedBy("lock") + @NonNull + private final InputStream mPlainInputStream; + + /** + * The output stream that this class uses to read from the socket. + */ + @GuardedBy("lock") + @NonNull + private final OutputStream mPlainOutputStream; + + /** + * The input stream that this class uses to read from the TLS socket. + */ + @GuardedBy("lock") + @Nullable + private volatile InputStream mTlsInputStream; + + /** + * The output stream that this class uses to read from the TLS socket. + */ + @GuardedBy("lock") + @Nullable + private volatile OutputStream mTlsOutputStream; + + /** + * The backend thread that handles responding to ADB packets. + */ + @NonNull + private final Thread mConnectionThread; + + /** + * Specifies whether a CNXN has been attempted. + */ + private volatile boolean mConnectAttempted; + + /** + * Whether the connection thread should give up if the first authentication attempt fails. + */ + private volatile boolean mAbortOnUnauthorised; + + /** + * Whether the first authentication attempt failed and {@link #mAbortOnUnauthorised} was {@code true}. + */ + private volatile boolean mAuthorisationFailed; + + /** + * Specifies whether a CNXN packet has been received from the peer. + */ + private volatile boolean mConnectionEstablished; + + /** + * Exceptions that occur in {@link #createConnectionThread()}. + */ + @Nullable + private volatile Exception mConnectionException; + + /** + * Specifies the maximum amount data that can be sent to the remote peer. + * This is only valid after connect() returns successfully. + */ + private volatile int mMaxData; + + private volatile int mProtocolVersion; + + @NonNull + private final KeyPair mKeyPair; + + @NonNull + private volatile String mDeviceName = "Unknown Device"; + + /** + * Specifies whether this connection has already sent a signed token. + */ + private volatile boolean mSentSignature; + + /** + * A hash map of our opened streams indexed by local ID. + */ + @NonNull + private final ConcurrentHashMap mOpenedStreams; + + private volatile boolean mIsTls = false; + + @GuardedBy("lock") + @NonNull + private final Object mLock = new Object(); + + /** + * Creates a AdbConnection object associated with the socket and crypto object specified. + * + * @return A new AdbConnection object. + * @throws IOException If there is a socket error + */ + @WorkerThread + @NonNull + public static AdbConnection create(@NonNull String host, int port, @NonNull PrivateKey privateKey, + @NonNull Certificate certificate) + throws IOException { + return create(host, port, privateKey, certificate, Build.VERSION_CODES.BASE); + } + + /** + * Creates a AdbConnection object associated with the socket and crypto object specified. + * + * @return A new AdbConnection object. + * @throws IOException If there is a socket error + */ + @WorkerThread + @NonNull + public static AdbConnection create(@NonNull String host, int port, @NonNull PrivateKey privateKey, + @NonNull Certificate certificate, int api) + throws IOException { + return create(host, port, new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)), + api); + } + + /** + * Creates a AdbConnection object associated with the socket and crypto object specified. + * + * @return A new AdbConnection object. + * @throws IOException If there is a socket error + */ + @WorkerThread + @NonNull + static AdbConnection create(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException { + return new AdbConnection(host, port, keyPair, api); + } + + /** + * Internal constructor to initialize some internal state + */ + @WorkerThread + private AdbConnection(@NonNull String host, int port, @NonNull KeyPair keyPair, int api) throws IOException { + this.mHost = Objects.requireNonNull(host); + this.mPort = port; + this.mApi = api; + this.mProtocolVersion = AdbProtocol.getProtocolVersion(mApi); + this.mMaxData = AdbProtocol.getMaxData(api); + this.mKeyPair = Objects.requireNonNull(keyPair); + try { + this.mSocket = new Socket(host, port); + } catch (Throwable th) { + //noinspection UnnecessaryInitCause + throw (IOException) new IOException().initCause(th); + } + this.mPlainInputStream = mSocket.getInputStream(); + this.mPlainOutputStream = mSocket.getOutputStream(); + + // Disable Nagle because we're sending tiny packets + mSocket.setTcpNoDelay(true); + + this.mOpenedStreams = new ConcurrentHashMap<>(); + this.mLastLocalId = 0; + this.mConnectionThread = createConnectionThread(); + } + + @GuardedBy("lock") + @NonNull + private InputStream getInputStream() { + return mIsTls ? Objects.requireNonNull(mTlsInputStream) : mPlainInputStream; + } + + @GuardedBy("lock") + @NonNull + private OutputStream getOutputStream() { + return mIsTls ? Objects.requireNonNull(mTlsOutputStream) : mPlainOutputStream; + } + + /** + * Creates a new connection thread. + * + * @return A new connection thread. + */ + @NonNull + private Thread createConnectionThread() { + return new Thread(() -> { + loop: + while (!mConnectionThread.isInterrupted()) { + try { + // Read and parse a message off the socket's input stream + AdbProtocol.Message msg = AdbProtocol.Message.parse(getInputStream(), mProtocolVersion, mMaxData); + + switch (msg.command) { + // Stream-oriented commands + case AdbProtocol.A_OKAY: + case AdbProtocol.A_WRTE: + case AdbProtocol.A_CLSE: { + // Ignore all packets when not connected + if (!mConnectionEstablished) { + continue; + } + + // Get the stream object corresponding to the packet + AdbStream waitingStream = mOpenedStreams.get(msg.arg1); + if (waitingStream == null) { + continue; + } + + synchronized (waitingStream) { + if (msg.command == AdbProtocol.A_OKAY) { + // We're ready for writes + waitingStream.updateRemoteId(msg.arg0); + waitingStream.readyForWrite(); + + // Notify an open/write + waitingStream.notify(); + } else if (msg.command == AdbProtocol.A_WRTE) { + // Got some data from our partner + waitingStream.addPayload(msg.payload); + + // Tell it we're ready for more + waitingStream.sendReady(); + } else { // if (msg.command == AdbProtocol.A_CLSE) { + mOpenedStreams.remove(msg.arg1); + // Notify readers and writers + waitingStream.notifyClose(true); + } + } + break; + } + case AdbProtocol.A_STLS: { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + sendPacket(AdbProtocol.generateStls()); + + SSLContext sslContext = SslUtils.getSslContext(mKeyPair); + SSLSocket tlsSocket = (SSLSocket) sslContext.getSocketFactory() + .createSocket(mSocket, mHost, mPort, true); + tlsSocket.startHandshake(); + Log.d(TAG, "Handshake succeeded."); + + synchronized (AdbConnection.this) { + mTlsInputStream = tlsSocket.getInputStream(); + mTlsOutputStream = tlsSocket.getOutputStream(); + mIsTls = true; + } + } + break; + } + case AdbProtocol.A_AUTH: { + if (mIsTls) { + break; + } + if (msg.arg0 != AdbProtocol.ADB_AUTH_TOKEN) { + break; + } + byte[] packet; + // This is an authentication challenge + if (mSentSignature) { + if (mAbortOnUnauthorised) { + mAuthorisationFailed = true; + break loop; + } + + // We've already tried our signature, so send our public key + packet = AdbProtocol.generateAuth(AdbProtocol.ADB_AUTH_RSAPUBLICKEY, AndroidPubkey + .encodeWithName((RSAPublicKey) mKeyPair.getPublicKey(), mDeviceName)); + } else { + // Sign the token + packet = AdbProtocol.generateAuth(AdbProtocol.ADB_AUTH_SIGNATURE, AndroidPubkey + .adbAuthSign(mKeyPair.getPrivateKey(), msg.payload)); + mSentSignature = true; + } + + // Write the AUTH reply + sendPacket(packet); + break; + } + case AdbProtocol.A_CNXN: { + synchronized (AdbConnection.this) { + mProtocolVersion = msg.arg0; + mMaxData = msg.arg1; + mConnectionEstablished = true; + AdbConnection.this.notifyAll(); + } + break; + } + case AdbProtocol.A_OPEN: + case AdbProtocol.A_SYNC: + default: + Log.e(TAG, String.format("Unrecognized command = 0x%x", msg.command)); + // Unrecognized packet, just drop it + break; + } + } catch (Exception e) { + mConnectionException = e; + e.printStackTrace(); + // The cleanup is taken care of by a combination of this thread and close() + break; + } + } + + // This thread takes care of cleaning up pending streams + synchronized (AdbConnection.this) { + cleanupStreams(); + AdbConnection.this.notifyAll(); + mConnectionEstablished = false; + mConnectAttempted = false; + } + }); + } + + /** + * Set a name for the device. Default is “Unknown Device”. + * + * @param deviceName Name of the device, could be the app label, hostname or user@hostname. + */ + public void setDeviceName(@NonNull String deviceName) { + this.mDeviceName = Objects.requireNonNull(deviceName); + } + + /** + * Get the version of the ADB protocol supported by the ADB daemon. The result may depend on the API version + * specified and whether the connection has been established. In API 29 (Android 9) or later, the daemon returns + * {@link AdbProtocol#A_VERSION_SKIP_CHECKSUM} regardless of the protocol used to create the connection. So, if + * {@link #mApi} is set to API 28 or earlier but the OS version is Android 9 or later, before establishing the + * connection, it returns {@link AdbProtocol#A_VERSION_MIN}, and after establishing the connection, it returns + * {@link AdbProtocol#A_VERSION_SKIP_CHECKSUM}. In other cases, it always returns {@link AdbProtocol#A_VERSION_MIN}. + * + * @see #isConnectionEstablished() + */ + public int getProtocolVersion() { + return mProtocolVersion; + } + + /** + * Get the max data size supported by the ADB daemon. A connection have to be attempted before calling this method + * and shall be blocked if the connection is in progress. + * + * @return The maximum data size indicated in the CONNECT packet. + * @throws InterruptedException If a connection cannot be waited on. + * @throws IOException if the connection fails. + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + public int getMaxData() throws InterruptedException, IOException, AdbPairingRequiredException { + if (!mConnectAttempted) { + throw new IllegalStateException("connect() must be called first"); + } + + waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + + return mMaxData; + } + + /** + * Whether a connection has been established. A connection has been established if a CONNECT request has been + * received from the ADB daemon. + */ + public boolean isConnectionEstablished() { + return mConnectionEstablished; + } + + /** + * Whether the underlying socket is connected to an ADB daemon and is not in a closed state. + */ + public boolean isConnected() { + return !mSocket.isClosed() && mSocket.isConnected(); + } + + /** + * Same as {@link #connect(long, TimeUnit, boolean)} without throwing anything if the first authentication attempt + * fails. + * + * @return {@code true} if the connection was established, or {@code false} if the connection timed out + * @throws IOException If the socket fails while connecting + * @throws InterruptedException If timeout has reached + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + public boolean connect() throws IOException, InterruptedException, AdbPairingRequiredException { + return connect(Long.MAX_VALUE, TimeUnit.MILLISECONDS, false); + } + + /** + * Connects to the remote device. This routine will block until the connection completes or the timeout elapses. + * + * @param timeout the time to wait for the lock + * @param unit the time unit of the timeout argument + * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException} + * if the peer rejects out first authentication attempt + * @return {@code true} if the connection was established, or {@code false} if the connection timed out + * @throws IOException If the socket fails while connecting + * @throws InterruptedException If timeout has reached + * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects the + * first authentication attempt, which indicates that the peer has not + * saved the public key from a previous connection + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + public boolean connect(long timeout, @NonNull TimeUnit unit, boolean throwOnUnauthorised) + throws IOException, InterruptedException, AdbAuthenticationFailedException, AdbPairingRequiredException { + if (mConnectionEstablished) { + throw new IllegalStateException("Already connected"); + } + + // Send CONNECT + sendPacket(AdbProtocol.generateConnect(mApi)); + + // Start the connection thread to respond to the peer + mConnectAttempted = true; + mAbortOnUnauthorised = throwOnUnauthorised; + mAuthorisationFailed = false; + mConnectionThread.start(); + + return waitForConnection(timeout, Objects.requireNonNull(unit)); + } + + /** + * Opens an {@link AdbStream} object corresponding to the specified destination. + * This routine will block until the connection completes. + * + * @param service The service to open. One of the services under {@link LocalServices.Services}. + * @param args Additional arguments supported by the service (see the corresponding constant to learn more). + * @return AdbStream object corresponding to the specified destination + * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 + * @throws IOException If the stream fails while sending the packet + * @throws InterruptedException If we are unable to wait for the connection to finish + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @NonNull + public AdbStream open(@LocalServices.Services int service, @NonNull String... args) + throws IOException, InterruptedException, AdbPairingRequiredException { + if (service < LocalServices.SERVICE_FIRST || service > LocalServices.SERVICE_LAST) { + throw new IllegalArgumentException("Invalid service: " + service); + } + return open(LocalServices.getDestination(service, args)); + } + + /** + * Opens an AdbStream object corresponding to the specified destination. + * This routine will block until the connection completes. + * + * @param destination The destination to open on the target + * @return AdbStream object corresponding to the specified destination + * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 + * @throws IOException If the stream fails while sending the packet + * @throws InterruptedException If we are unable to wait for the connection to finish + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + @NonNull + public AdbStream open(@NonNull String destination) + throws IOException, InterruptedException, AdbPairingRequiredException { + int localId = ++mLastLocalId; + + if (!mConnectAttempted) { + throw new IllegalStateException("connect() must be called first"); + } + + waitForConnection(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + + // Add this stream to this list of half-open streams + AdbStream stream = new AdbStream(this, localId); + mOpenedStreams.put(localId, stream); + + // Send OPEN + sendPacket(AdbProtocol.generateOpen(localId, Objects.requireNonNull(destination))); + + // Wait for the connection thread to receive the OKAY + synchronized (stream) { + stream.wait(); + } + + // Check if the OPEN request was rejected + if (stream.isClosed()) { + mOpenedStreams.remove(localId); + throw new ConnectException("Stream open actively rejected by remote peer."); + } + + return stream; + } + + private boolean waitForConnection(long timeout, @NonNull TimeUnit unit) + throws InterruptedException, IOException, AdbPairingRequiredException { + synchronized (this) { + // Block if a connection is pending, but not yet complete + long timeoutEndMillis = System.currentTimeMillis() + Objects.requireNonNull(unit).toMillis(timeout); + while (!mConnectionEstablished && mConnectAttempted && timeoutEndMillis - System.currentTimeMillis() > 0) { + wait(timeoutEndMillis - System.currentTimeMillis()); + } + + if (!mConnectionEstablished) { + if (mConnectAttempted) { + return false; + } else if (mAuthorisationFailed) { + // The peer may not have saved the public key in the past connections, or they've been removed. + throw new AdbAuthenticationFailedException(); + } else { + Exception connectionException = mConnectionException; + if (connectionException != null) { + if (connectionException instanceof javax.net.ssl.SSLProtocolException) { + String message = connectionException.getMessage(); + if (message != null && message.contains("protocol error")) { + throw (AdbPairingRequiredException) (new AdbPairingRequiredException("ADB pairing is required.").initCause(connectionException)); + } + } + } + throw new IOException("Connection failed"); + } + } + } + + return true; + } + + /** + * This function terminates all I/O on streams associated with this ADB connection + */ + private void cleanupStreams() { + // Close all streams on this connection + for (AdbStream s : mOpenedStreams.values()) { + try { + s.close(); + } catch (IOException ignored) { + } + } + mOpenedStreams.clear(); + } + + /** + * This routine closes the Adb connection and underlying socket + * + * @throws IOException if the socket fails to close + */ + @Override + public void close() throws IOException { + // Closing the socket will kick the connection thread + mSocket.close(); + + // Wait for the connection thread to die + mConnectionThread.interrupt(); + try { + mConnectionThread.join(); + } catch (InterruptedException ignored) { + } + + // Destroy keypair + try { + mKeyPair.destroy(); + } catch (DestroyFailedException ignore) { + } + } + + void sendPacket(byte[] packet) throws IOException { + synchronized (mLock) { + OutputStream os = getOutputStream(); + os.write(packet); + os.flush(); + } + } + + void flushPacket() throws IOException { + synchronized (mLock) { + getOutputStream().flush(); + } + } + + public static class Builder { + private String mHost = "127.0.0.1"; + private int mPort = 5555; + private int mApi = Build.VERSION_CODES.BASE; + private PrivateKey mPrivateKey; + private Certificate mCertificate; + private KeyPair mKeyPair; + private String mDeviceName; + + public Builder() { + } + + public Builder(String host, int port) { + mHost = host; + mPort = port; + } + + /** + * Set host address. Default is 127.0.0.1 + */ + public Builder setHost(String host) { + this.mHost = host; + return this; + } + + /** + * Set port number. Default is 5555. + */ + public Builder setPort(int port) { + this.mPort = port; + return this; + } + + /** + * Set a name for the device. Default is “Unknown Device”. + * + * @param deviceName Name of the device, could be the app label, hostname or user@hostname. + */ + public Builder setDeviceName(String deviceName) { + this.mDeviceName = deviceName; + return this; + } + + /** + * Set Android API (i.e. SDK) version for this connection. If the ADB daemon and the client are located in the + * same device, the value should be {@link Build.VERSION#SDK_INT} in order to improve performance as well as + * security. + * + * @param api The API version, default is {@link Build.VERSION_CODES#BASE}. + */ + public Builder setApi(int api) { + this.mApi = api; + return this; + } + + /** + * Set generated/stored private key. + */ + public Builder setPrivateKey(PrivateKey privateKey) { + this.mPrivateKey = privateKey; + return this; + } + + /** + * Set public key wrapped around a certificate + */ + public Builder setCertificate(Certificate certificate) { + this.mCertificate = certificate; + return this; + } + + Builder setKeyPair(KeyPair keyPair) { + this.mKeyPair = keyPair; + return this; + } + + /** + * Creates a new {@link AdbConnection} associated with the socket and crypto object specified. + * + * @throws IOException If there was an error while establishing a socket connection + */ + public AdbConnection build() throws IOException { + if (mKeyPair == null) { + if (mPrivateKey == null || mCertificate == null) { + throw new UnsupportedOperationException("Private key and certificate must be set."); + } + mKeyPair = new KeyPair(mPrivateKey, mCertificate); + } + AdbConnection adbConnection = create(mHost, mPort, mKeyPair, mApi); + if (mDeviceName != null) { + adbConnection.setDeviceName(mDeviceName); + } + return adbConnection; + } + + /** + * Same as {@link #connect(long, TimeUnit, boolean)} without throwing anything if the first authentication + * attempt fails. + * + * @return The underlying {@link AdbConnection} + * @throws IOException If the socket fails while connecting + * @throws InterruptedException If timeout has reached + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + public AdbConnection connect() throws IOException, InterruptedException, AdbPairingRequiredException { + AdbConnection adbConnection = build(); + if (adbConnection.connect()) { + throw new IOException("Unable to establish a new connection."); + } + return adbConnection; + } + + /** + * Connects to the remote device. This routine will block until the connection completes or the timeout elapses. + * + * @param timeout the time to wait for the lock + * @param unit the time unit of the timeout argument + * @param throwOnUnauthorised Whether to throw an {@link AdbAuthenticationFailedException} + * if the peer rejects out first authentication attempt + * @return {@code true} if the connection was established, or {@code false} if the connection timed out + * @throws IOException If the socket fails while connecting + * @throws InterruptedException If timeout has reached + * @throws AdbAuthenticationFailedException If {@code throwOnUnauthorised} is {@code true} and the peer rejects + * the first authentication attempt, which indicates that the peer has + * not saved the public key from a previous connection + * @throws AdbPairingRequiredException If ADB lacks pairing + */ + public AdbConnection connect(long timeout, @NonNull TimeUnit unit, boolean throwOnUnauthorised) + throws IOException, InterruptedException, AdbPairingRequiredException { + AdbConnection adbConnection = build(); + if (adbConnection.connect(timeout, unit, throwOnUnauthorised)) { + throw new IOException("Unable to establish a new connection."); + } + return adbConnection; + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java new file mode 100644 index 0000000..4b5fa20 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbConnectionManager.java @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.content.Context; +import android.os.Build; +import android.sun.misc.BASE64Encoder; +import android.sun.security.provider.X509Factory; +import android.sun.security.x509.AlgorithmId; +import android.sun.security.x509.CertificateAlgorithmId; +import android.sun.security.x509.CertificateExtensions; +import android.sun.security.x509.CertificateIssuerName; +import android.sun.security.x509.CertificateSerialNumber; +import android.sun.security.x509.CertificateSubjectName; +import android.sun.security.x509.CertificateValidity; +import android.sun.security.x509.CertificateVersion; +import android.sun.security.x509.CertificateX509Key; +import android.sun.security.x509.KeyIdentifier; +import android.sun.security.x509.PrivateKeyUsageExtension; +import android.sun.security.x509.SubjectKeyIdentifierExtension; +import android.sun.security.x509.X500Name; +import android.sun.security.x509.X509CertImpl; +import android.sun.security.x509.X509CertInfo; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.spec.EncodedKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Date; +import java.util.Random; + + +public class AdbConnectionManager extends AbsAdbConnectionManager { + private static AbsAdbConnectionManager INSTANCE; + + public static AbsAdbConnectionManager getInstance(@NonNull Context context) throws Exception { + if (INSTANCE == null) { + INSTANCE = new AdbConnectionManager(context); + } + return INSTANCE; + } + + private PrivateKey mPrivateKey; + private Certificate mCertificate; + + private AdbConnectionManager(@NonNull Context context) throws Exception { + setApi(Build.VERSION.SDK_INT); + mPrivateKey = readPrivateKeyFromFile(context); + mCertificate = readCertificateFromFile(context); + if (mPrivateKey == null) { + // Generate a new key pair + int keySize = 2048; + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG")); + KeyPair generateKeyPair = keyPairGenerator.generateKeyPair(); + PublicKey publicKey = generateKeyPair.getPublic(); + mPrivateKey = generateKeyPair.getPrivate(); + // Generate a new certificate + String subject = "CN=My Awesome App"; + String algorithmName = "SHA512withRSA"; + long expiryDate = System.currentTimeMillis() + 86400000; + CertificateExtensions certificateExtensions = new CertificateExtensions(); + certificateExtensions.set("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension( + new KeyIdentifier(publicKey).getIdentifier())); + X500Name x500Name = new X500Name(subject); + Date notBefore = new Date(); + Date notAfter = new Date(expiryDate); + certificateExtensions.set("PrivateKeyUsage", new PrivateKeyUsageExtension(notBefore, notAfter)); + CertificateValidity certificateValidity = new CertificateValidity(notBefore, notAfter); + X509CertInfo x509CertInfo = new X509CertInfo(); + x509CertInfo.set("version", new CertificateVersion(2)); + x509CertInfo.set("serialNumber", new CertificateSerialNumber(new Random().nextInt() & Integer.MAX_VALUE)); + x509CertInfo.set("algorithmID", new CertificateAlgorithmId(AlgorithmId.get(algorithmName))); + x509CertInfo.set("subject", new CertificateSubjectName(x500Name)); + x509CertInfo.set("key", new CertificateX509Key(publicKey)); + x509CertInfo.set("validity", certificateValidity); + x509CertInfo.set("issuer", new CertificateIssuerName(x500Name)); + x509CertInfo.set("extensions", certificateExtensions); + X509CertImpl x509CertImpl = new X509CertImpl(x509CertInfo); + x509CertImpl.sign(mPrivateKey, algorithmName); + mCertificate = x509CertImpl; + // Write files + writePrivateKeyToFile(context, mPrivateKey); + writeCertificateToFile(context, mCertificate); + } + } + + @NonNull + @Override + protected PrivateKey getPrivateKey() { + return mPrivateKey; + } + + @NonNull + @Override + protected Certificate getCertificate() { + return mCertificate; + } + + @NonNull + @Override + protected String getDeviceName() { + return "MyAwesomeApp"; + } + + @Nullable + private static Certificate readCertificateFromFile(@NonNull Context context) + throws IOException, CertificateException { + File certFile = new File(context.getFilesDir(), "cert.pem"); + if (!certFile.exists()) return null; + try (InputStream cert = new FileInputStream(certFile)) { + return CertificateFactory.getInstance("X.509").generateCertificate(cert); + } + } + + private static void writeCertificateToFile(@NonNull Context context, @NonNull Certificate certificate) + throws CertificateEncodingException, IOException { + File certFile = new File(context.getFilesDir(), "cert.pem"); + BASE64Encoder encoder = new BASE64Encoder(); + try (OutputStream os = new FileOutputStream(certFile)) { + os.write(X509Factory.BEGIN_CERT.getBytes(StandardCharsets.UTF_8)); + os.write('\n'); + encoder.encode(certificate.getEncoded(), os); + os.write('\n'); + os.write(X509Factory.END_CERT.getBytes(StandardCharsets.UTF_8)); + } + } + + @Nullable + private static PrivateKey readPrivateKeyFromFile(@NonNull Context context) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + File privateKeyFile = new File(context.getFilesDir(), "private.key"); + if (!privateKeyFile.exists()) return null; + byte[] privKeyBytes = new byte[(int) privateKeyFile.length()]; + try (InputStream is = new FileInputStream(privateKeyFile)) { + is.read(privKeyBytes); + } + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes); + return keyFactory.generatePrivate(privateKeySpec); + } + + private static void writePrivateKeyToFile(@NonNull Context context, @NonNull PrivateKey privateKey) + throws IOException { + File privateKeyFile = new File(context.getFilesDir(), "private.key"); + try (OutputStream os = new FileOutputStream(privateKeyFile)) { + os.write(privateKey.getEncoded()); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java new file mode 100644 index 0000000..eabcc36 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import java.io.IOException; +import java.io.InputStream; + +public class AdbInputStream extends InputStream { + public AdbStream mAdbStream; + + public AdbInputStream(AdbStream adbStream) { + this.mAdbStream = adbStream; + } + + @Override + public int read() throws IOException { + byte[] bytes = new byte[1]; + if (read(bytes) == -1) { + return -1; + } + return bytes[0]; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (mAdbStream.isClosed()) return -1; + return mAdbStream.read(b, off, len); + } + + @Override + public void close() { + } + + @Override + public int available() throws IOException { + return mAdbStream.available(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java new file mode 100644 index 0000000..6eea18e --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import java.io.IOException; +import java.io.OutputStream; + +public class AdbOutputStream extends OutputStream { + private final AdbStream mAdbStream; + + public AdbOutputStream(AdbStream adbStream) { + this.mAdbStream = adbStream; + } + + @Override + public void write(int b) throws IOException { + write(new byte[]{(byte) (b & 0xFF)}); + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + mAdbStream.write(b, off, len); + } + + @Override + public void flush() throws IOException { + mAdbStream.flush(); + } + + @Override + public void close() throws IOException { + flush(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java new file mode 100644 index 0000000..59e0e57 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java @@ -0,0 +1,7 @@ +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +public class AdbPairingRequiredException extends Exception { + public AdbPairingRequiredException(String message) { + super(message); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java new file mode 100644 index 0000000..fe56823 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.os.Build; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +/** + * This class provides useful functions and fields for ADB protocol details. + */ +// Copyright 2013 Cameron Gutman +final class AdbProtocol { + /** + * The length of the ADB message header + */ + public static final int ADB_HEADER_LENGTH = 24; + + /** + * SYNC(online, sequence, "") + * + * @deprecated Obsolete, no longer used. Never used on the client side. + */ + public static final int A_SYNC = 0x434e5953; + + /** + * CNXN is the connect message. No messages (except AUTH) are valid before this message is received. + */ + public static final int A_CNXN = 0x4e584e43; + + /** + * The payload sent with the CONNECT message. + */ + public static final byte[] SYSTEM_IDENTITY_STRING_HOST = StringCompat.getBytes("host::\0", "UTF-8"); + + /** + * AUTH is the authentication message. It is part of the RSA public key authentication added in Android 4.2.2 + * ({@link Build.VERSION_CODES#JELLY_BEAN_MR1}). + */ + public static final int A_AUTH = 0x48545541; + + /** + * OPEN is the open stream message. It is sent to open a new stream on the target device. + */ + public static final int A_OPEN = 0x4e45504f; + + /** + * OKAY is a success message. It is sent when a write is processed successfully. + */ + public static final int A_OKAY = 0x59414b4f; + + /** + * CLSE is the close stream message. It is sent to close an existing stream on the target device. + */ + public static final int A_CLSE = 0x45534c43; + + /** + * WRTE is the write stream message. It is sent with a payload that is the data to write to the stream. + */ + public static final int A_WRTE = 0x45545257; + + /** + * STLS is the Stream-based TLS1.3 authentication method, added in Android 9 ({@link Build.VERSION_CODES#P}). + */ + public static final int A_STLS = 0x534c5453; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({A_SYNC, A_CNXN, A_OPEN, A_OKAY, A_CLSE, A_WRTE, A_AUTH, A_STLS}) + private @interface Command { + } + + /** + * Original payload size + */ + public static final int MAX_PAYLOAD_V1 = 4 * 1024; + /** + * Supported payload size since Android 7 (N) + */ + public static final int MAX_PAYLOAD_V2 = 256 * 1024; + /** + * Supported payload size since Android 9 (P) + */ + public static final int MAX_PAYLOAD_V3 = 1024 * 1024; + /** + * Maximum supported payload size is set to the original to support all APIs + */ + public static final int MAX_PAYLOAD = MAX_PAYLOAD_V1; + + /** + * The original version of the ADB protocol + */ + public static final int A_VERSION_MIN = 0x01000000; + /** + * The new version of the ADB protocol introduced in Android 9 (P) with the introduction of TLS + */ + public static final int A_VERSION_SKIP_CHECKSUM = 0x01000001; + public static final int A_VERSION = A_VERSION_MIN; + + /** + * The current version of the Stream-based TLS + */ + public static final int A_STLS_VERSION_MIN = 0x01000000; + public static final int A_STLS_VERSION = A_STLS_VERSION_MIN; + + /** + * This authentication type represents a SHA1 hash to sign. + */ + public static final int ADB_AUTH_TOKEN = 1; + + /** + * This authentication type represents the signed SHA1 hash. + */ + public static final int ADB_AUTH_SIGNATURE = 2; + + /** + * This authentication type represents an RSA public key. + */ + public static final int ADB_AUTH_RSAPUBLICKEY = 3; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ADB_AUTH_TOKEN, ADB_AUTH_SIGNATURE, ADB_AUTH_RSAPUBLICKEY}) + private @interface AuthType { + } + + public static int getMaxData(int api) { + if (api >= Build.VERSION_CODES.P) { + return MAX_PAYLOAD_V3; + } + if (api >= Build.VERSION_CODES.N) { + return MAX_PAYLOAD_V2; + } + return MAX_PAYLOAD_V1; + } + + public static int getProtocolVersion(int api) { + if (api >= Build.VERSION_CODES.P) { + return A_VERSION_SKIP_CHECKSUM; + } + return A_VERSION_MIN; + } + + /** + * This function performs a checksum on the ADB payload data. + * + * @param data The data + * @return The checksum of the data + */ + private static int getPayloadChecksum(@NonNull byte[] data) { + return getPayloadChecksum(data, 0, data.length); + } + + /** + * This function performs a checksum on the ADB payload data. + * + * @param data The data + * @param offset The start offset in the data + * @param length The number of bytes to take from the data + * @return The checksum of the data + */ + private static int getPayloadChecksum(@NonNull byte[] data, int offset, int length) { + int checksum = 0; + for (int i = offset; i < offset + length; ++i) { + checksum += data[i] & 0xFF; + } + return checksum; + } + + /** + * This function generates an ADB message given the fields. + * + * @param command Command identifier constant + * @param arg0 First argument + * @param arg1 Second argument + * @param data The data + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data) { + return generateMessage(command, arg0, arg1, data, 0, data == null ? 0 : data.length); + } + + /** + * This function generates an ADB message given the fields. + * + * @param command Command identifier constant + * @param arg0 First argument + * @param arg1 Second argument + * @param data The data + * @param offset The start offset in the data + * @param length The number of bytes to take from the data + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data, int offset, int length) { + // Protocol as defined at https://github.com/aosp-mirror/platform_system_core/blob/6072de17cd812daf238092695f26a552d3122f8c/adb/protocol.txt + // struct message { + // unsigned command; // command identifier constant + // unsigned arg0; // first argument + // unsigned arg1; // second argument + // unsigned data_length; // length of payload (0 is allowed) + // unsigned data_check; // checksum of data payload + // unsigned magic; // command ^ 0xffffffff + // }; + + ByteBuffer message; + + if (data != null) { + message = ByteBuffer.allocate(ADB_HEADER_LENGTH + length).order(ByteOrder.LITTLE_ENDIAN); + } else { + message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); + } + + message.putInt(command); + message.putInt(arg0); + message.putInt(arg1); + + if (data != null) { + message.putInt(length); + message.putInt(getPayloadChecksum(data, offset, length)); + } else { + message.putInt(0); + message.putInt(0); + } + + message.putInt(~command); + + if (data != null) { + message.put(data, offset, length); + } + + return message.array(); + } + + /** + * Generates a CONNECT message for a given API. + *

+ * CONNECT(version, maxdata, "system-identity-string") + * + * @param api API version + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateConnect(int api) { + return generateMessage(A_CNXN, getProtocolVersion(api), getMaxData(api), SYSTEM_IDENTITY_STRING_HOST); + } + + /** + * Generates an AUTH message with the specified type and payload. + *

+ * AUTH(type, 0, "data") + * + * @param type Authentication type (see ADB_AUTH_* constants) + * @param data The data + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateAuth(@AuthType int type, byte[] data) { + return generateMessage(A_AUTH, type, 0, data); + } + + /** + * Generates an STLS message with default parameters. + *

+ * STLS(version, 0, "") + * + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateStls() { + return generateMessage(A_STLS, A_STLS_VERSION, 0, null); + } + + /** + * Generates an OPEN stream message with the specified local ID and destination. + *

+ * OPEN(local-id, 0, "destination") + * + * @param localId A unique local ID identifying the stream + * @param destination The destination of the stream on the target + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateOpen(int localId, @NonNull String destination) { + ByteBuffer bbuf = ByteBuffer.allocate(destination.length() + 100); + bbuf.put(StringCompat.getBytes(destination, "UTF-8")); + bbuf.put((byte) 0); + return generateMessage(A_OPEN, localId, 0, bbuf.array()); + } + + /** + * Generates a WRITE stream message with the specified IDs and payload. + *

+ * WRITE(local-id, remote-id, "data") + * + * @param localId The unique local ID of the stream + * @param remoteId The unique remote ID of the stream + * @param data The data + * @param offset The start offset in the data + * @param length The number of bytes to take from the data + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateWrite(int localId, int remoteId, byte[] data, int offset, int length) { + return generateMessage(A_WRTE, localId, remoteId, data, offset, length); + } + + /** + * Generates a CLOSE stream message with the specified IDs. + *

+ * CLOSE(local-id, remote-id, "") + * + * @param localId The unique local ID of the stream + * @param remoteId The unique remote ID of the stream + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateClose(int localId, int remoteId) { + return generateMessage(A_CLSE, localId, remoteId, null); + } + + /** + * Generates an OKAY/READY message with the specified IDs. + *

+ * READY(local-id, remote-id, "") + * + * @param localId The unique local ID of the stream + * @param remoteId The unique remote ID of the stream + * @return Byte array containing the message + */ + @NonNull + public static byte[] generateReady(int localId, int remoteId) { + return generateMessage(A_OKAY, localId, remoteId, null); + } + + /** + * This class provides an abstraction for the ADB message format. + */ + static final class Message { + /** + * The command field of the message + */ + @Command + public final int command; + /** + * The arg0 field of the message + */ + public final int arg0; + /** + * The arg1 field of the message + */ + public final int arg1; + /** + * The payload length field of the message + */ + public final int dataLength; + /** + * The checksum field of the message + */ + public final int dataCheck; + /** + * The magic field of the message + */ + public final int magic; + /** + * The payload of the message + */ + public byte[] payload; + + /** + * Read and parse an ADB message from the supplied input stream. + *

+ * Note: If data is corrupted, the connection has to be closed immediately to avoid inconsistencies. + * + * @param in InputStream object to read data from + * @return An AdbMessage object represented the message read + * @throws IOException If the stream fails while reading. + * @throws StreamCorruptedException If data is corrupted. + */ + @NonNull + public static Message parse(@NonNull InputStream in, int protocolVersion, int maxData) throws IOException { + ByteBuffer header = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); + + // Read header + int dataRead = 0; + do { + int bytesRead = in.read(header.array(), dataRead, ADB_HEADER_LENGTH - dataRead); + if (bytesRead < 0) { + throw new IOException("Stream closed"); + } else dataRead += bytesRead; + } while (dataRead < ADB_HEADER_LENGTH); + + Message msg = new Message(header); + + // Validate header + if (msg.command != (~msg.magic)) { // magic = cmd ^ 0xFFFFFFFF + throw new StreamCorruptedException(String.format("Invalid header: Invalid magic 0x%x.", msg.magic)); + } + if (msg.command != A_SYNC && msg.command != A_CNXN && msg.command != A_OPEN && msg.command != A_OKAY + && msg.command != A_CLSE && msg.command != A_WRTE && msg.command != A_AUTH + && msg.command != A_STLS) { + throw new StreamCorruptedException(String.format("Invalid header: Invalid command 0x%x.", msg.command)); + } + if (msg.dataLength < 0 || msg.dataLength > maxData) { + throw new StreamCorruptedException(String.format("Invalid header: Invalid data length %d", msg.dataLength)); + } + + if (msg.dataLength == 0) { + // No payload supplied, return immediately + return msg; + } + + // Read payload + msg.payload = new byte[msg.dataLength]; + dataRead = 0; + do { + int bytesRead = in.read(msg.payload, dataRead, msg.dataLength - dataRead); + if (bytesRead < 0) { + throw new IOException("Stream closed"); + } else dataRead += bytesRead; + } while (dataRead < msg.dataLength); + + // Verify payload + if ((protocolVersion <= A_VERSION_MIN || (msg.command == A_CNXN && msg.arg0 <= A_VERSION_MIN)) + && getPayloadChecksum(msg.payload) != msg.dataCheck) { + // Checksum verification failed + throw new StreamCorruptedException("Invalid header: Checksum mismatched."); + } + + return msg; + } + + private Message(@NonNull ByteBuffer header) { + command = header.getInt(); + arg0 = header.getInt(); + arg1 = header.getInt(); + dataLength = header.getInt(); + dataCheck = header.getInt(); + magic = header.getInt(); + } + + @NonNull + @Override + public String toString() { + String tag; + switch (command) { + case A_SYNC: + tag = "SYNC"; + break; + case A_CNXN: + tag = "CNXN"; + break; + case A_OPEN: + tag = "OPEN"; + break; + case A_OKAY: + tag = "OKAY"; + break; + case A_CLSE: + tag = "CLSE"; + break; + case A_WRTE: + tag = "WRTE"; + break; + case A_AUTH: + tag = "AUTH"; + break; + case A_STLS: + tag = "STLS"; + break; + default: + tag = "????"; + break; + } + return "Message{" + + "command=" + tag + + ", arg0=0x" + Integer.toHexString(arg0) + + ", arg1=0x" + Integer.toHexString(arg1) + + ", payloadLength=" + dataLength + + ", checksum=" + dataCheck + + ", magic=0x" + Integer.toHexString(magic) + + ", payload=" + Arrays.toString(payload) + + '}'; + } + } + +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java new file mode 100644 index 0000000..02a019a --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * This class abstracts the underlying ADB streams + */ +// Copyright 2013 Cameron Gutman +public class AdbStream implements Closeable { + + /** + * The AdbConnection object that the stream communicates over + */ + private final AdbConnection mAdbConnection; + + /** + * The local ID of the stream + */ + private final int mLocalId; + + /** + * The remote ID of the stream + */ + private volatile int mRemoteId; + + /** + * Indicates whether WRTE is currently allowed + */ + private final AtomicBoolean mWriteReady; + + /** + * A queue of data from the target's WRTE packets + */ + private final Queue mReadQueue; + + /** + * Store data received from the first WRTE packet in order to support buffering. + */ + private final ByteBuffer mReadBuffer; + + /** + * Indicates whether the connection is closed already + */ + private volatile boolean mIsClosed; + + /** + * Whether the remote peer has closed but we still have unread data in the queue + */ + private volatile boolean mPendingClose; + + /** + * Creates a new AdbStream object on the specified AdbConnection + * with the given local ID. + * + * @param adbConnection AdbConnection that this stream is running on + * @param localId Local ID of the stream + */ + AdbStream(AdbConnection adbConnection, int localId) + throws IOException, InterruptedException, AdbPairingRequiredException { + this.mAdbConnection = adbConnection; + this.mLocalId = localId; + this.mReadQueue = new ConcurrentLinkedQueue<>(); + this.mReadBuffer = (ByteBuffer) ByteBuffer.allocate(adbConnection.getMaxData()).flip(); + this.mWriteReady = new AtomicBoolean(false); + this.mIsClosed = false; + } + + public AdbInputStream openInputStream() { + return new AdbInputStream(this); + } + + public AdbOutputStream openOutputStream() { + return new AdbOutputStream(this); + } + + /** + * Called by the connection thread to indicate newly received data. + * + * @param payload Data inside the WRTE message + */ + void addPayload(byte[] payload) { + synchronized (mReadQueue) { + mReadQueue.add(payload); + mReadQueue.notifyAll(); + } + } + + /** + * Called by the connection thread to send an OKAY packet, allowing the + * other side to continue transmission. + * + * @throws IOException If the connection fails while sending the packet + */ + void sendReady() throws IOException { + // Generate and send a OKAY packet + mAdbConnection.sendPacket(AdbProtocol.generateReady(mLocalId, mRemoteId)); + } + + /** + * Called by the connection thread to update the remote ID for this stream + * + * @param remoteId New remote ID + */ + void updateRemoteId(int remoteId) { + this.mRemoteId = remoteId; + } + + /** + * Called by the connection thread to indicate the stream is okay to send data. + */ + void readyForWrite() { + mWriteReady.set(true); + } + + /** + * Called by the connection thread to notify that the stream was closed by the peer. + */ + void notifyClose(boolean closedByPeer) { + // We don't call close() because it sends another CLSE + if (closedByPeer && !mReadQueue.isEmpty()) { + // The remote peer closed the stream, but we haven't finished reading the remaining data + mPendingClose = true; + } else { + mIsClosed = true; + } + + // Notify readers and writers + synchronized (this) { + notifyAll(); + } + synchronized (mReadQueue) { + mReadQueue.notifyAll(); + } + } + + /** + * Read bytes from the ADB daemon. + * + * @return the next byte of data, or {@code -1} if the end of the stream is reached. + * @throws IOException If the stream fails while waiting + */ + public int read(byte[] bytes, int offset, int length) throws IOException { + if (mReadBuffer.hasRemaining()) { + return readBuffer(bytes, offset, length); + } + // Buffer has no data, grab from the queue + synchronized (mReadQueue) { + byte[] data; + // Wait for the connection to close or data to be received + while ((data = mReadQueue.poll()) == null && !mIsClosed) { + try { + mReadQueue.wait(); + } catch (InterruptedException e) { + //noinspection UnnecessaryInitCause + throw (IOException) new IOException().initCause(e); + } + } + // Add data to the buffer + if (data != null) { + mReadBuffer.clear(); + mReadBuffer.put(data); + mReadBuffer.flip(); + if (mReadBuffer.hasRemaining()) { + return readBuffer(bytes, offset, length); + } + } + + if (mIsClosed) { + throw new IOException("Stream closed."); + } + + if (mPendingClose && mReadQueue.isEmpty()) { + // The peer closed the stream, and we've finished reading the stream data, so this stream is finished + mIsClosed = true; + } + } + + return -1; + } + + private int readBuffer(byte[] bytes, int offset, int length) { + int count = 0; + for (int i = offset; i < offset + length; ++i) { + if (mReadBuffer.hasRemaining()) { + bytes[i] = mReadBuffer.get(); + ++count; + } + } + return count; + } + + /** + * Sends a WRTE packet with a given byte array payload. It does not flush the stream. + * + * @param bytes Payload in the form of a byte array + * @throws IOException If the stream fails while sending data + */ + public void write(byte[] bytes, int offset, int length) throws IOException { + synchronized (this) { + // Make sure we're ready for a WRTE + while (!mIsClosed && !mWriteReady.compareAndSet(true, false)) { + try { + wait(); + } catch (InterruptedException e) { + //noinspection UnnecessaryInitCause + throw (IOException) new IOException().initCause(e); + } + } + + if (mIsClosed) { + throw new IOException("Stream closed"); + } + } + // Split and send data as WRTE packet + // TODO: A WRITE message may not be sent until a READY message is received. + // Once a WRITE message is sent, an additional WRITE message may not be + // sent until another READY message has been received. Recipients of + // a WRITE message that is in violation of this requirement will CLOSE + // the connection. + int maxData; + try { + maxData = mAdbConnection.getMaxData(); + } catch (InterruptedException | AdbPairingRequiredException e) { + //noinspection UnnecessaryInitCause + throw (IOException) new IOException().initCause(e); + } + while (length != 0) { + if (length <= maxData) { + mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, length)); + offset = offset + length; + length = 0; + } else { // if (length > maxData) { + mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, maxData)); + offset = offset + maxData; + length = length - maxData; + } + } + } + + public void flush() throws IOException { + if (mIsClosed) { + throw new IOException("Stream closed"); + } + mAdbConnection.flushPacket(); + } + + /** + * Closes the stream. This sends a close message to the peer. + * + * @throws IOException If the stream fails while sending the close message. + */ + @Override + public void close() throws IOException { + synchronized (this) { + // This may already be closed by the remote host + if (mIsClosed) + return; + + // Notify readers/writers that we've closed + notifyClose(false); + } + + mAdbConnection.sendPacket(AdbProtocol.generateClose(mLocalId, mRemoteId)); + } + + /** + * Returns whether the stream is closed or not + * + * @return True if the stream is close, false if not + */ + public boolean isClosed() { + return mIsClosed; + } + + /** + * Returns an estimate of available data. + * + * @return an estimate of the number of bytes that can be read from this stream without blocking. + * @throws IOException if the stream is close. + */ + public int available() throws IOException { + synchronized (this) { + if (mIsClosed) { + throw new IOException("Stream closed."); + } + if (mReadBuffer.hasRemaining()) { + return mReadBuffer.remaining(); + } + byte[] data = mReadQueue.peek(); + return data == null ? 0 : data.length; + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java new file mode 100644 index 0000000..2e0496c --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java @@ -0,0 +1,245 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; +import java.util.Objects; + +import javax.crypto.Cipher; + +final class AndroidPubkey { + /** + * Size of an RSA modulus such as an encrypted block or a signature. + */ + public static final int ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8; + + /** + * Size of an encoded RSA key. + */ + public static final int ANDROID_PUBKEY_ENCODED_SIZE = 3 * 4 + 2 * ANDROID_PUBKEY_MODULUS_SIZE; + + /** + * Size of the RSA modulus in words. + */ + public static final int ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4; + + /** + * The RSA signature padding as an int array. + */ + private static final int[] SIGNATURE_PADDING_AS_INT = new int[]{ + 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, + 0x04, 0x14 + }; + + /** + * The RSA signature padding as a byte array + */ + private static final byte[] RSA_SHA_PKCS1_SIGNATURE_PADDING; + + static { + RSA_SHA_PKCS1_SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length]; + + for (int i = 0; i < RSA_SHA_PKCS1_SIGNATURE_PADDING.length; i++) + RSA_SHA_PKCS1_SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i]; + } + + /** + * Signs the ADB SHA1 payload with the private key of this object. + * + * @param privateKey Private key to sign with + * @param payload SHA1 payload to sign + * @return Signed SHA1 payload + * @throws GeneralSecurityException If signing fails + */ + // Taken from adb_auth_sign + @NonNull + public static byte[] adbAuthSign(@NonNull PrivateKey privateKey, byte[] payload) + throws GeneralSecurityException { + Cipher c = Cipher.getInstance("RSA/ECB/NoPadding"); + c.init(Cipher.ENCRYPT_MODE, privateKey); + c.update(RSA_SHA_PKCS1_SIGNATURE_PADDING); + return c.doFinal(payload); + } + + /** + * Converts a standard RSAPublicKey object to the special ADB format. Available since 4.2.2. + * + * @param publicKey RSAPublicKey object to convert + * @param name Name without null terminator + * @return Byte array containing the converted RSAPublicKey object + */ + @NonNull + public static byte[] encodeWithName(@NonNull RSAPublicKey publicKey, @NonNull String name) + throws InvalidKeyException { + int pkeySize = 4 * (int) Math.ceil(ANDROID_PUBKEY_ENCODED_SIZE / 3.0); + try (ByteArrayNoThrowOutputStream bos = new ByteArrayNoThrowOutputStream(pkeySize + name.length() + 2)) { + bos.write(Base64.encode(encode(publicKey),0)); + bos.write(getUserInfo(name)); + return bos.toByteArray(); + } + } + + // Taken from get_user_info except that a custom name is used instead of host@user + @VisibleForTesting + @NonNull + static byte[] getUserInfo(@NonNull String name) { + return StringCompat.getBytes(String.format(" %s\u0000", name), "UTF-8"); + } + + // https://android.googlesource.com/platform/system/core/+/e797a5c75afc17024d0f0f488c130128fcd704e2/libcrypto_utils/android_pubkey.cpp + // typedef struct RSAPublicKey { + // uint32_t modulus_size_words; // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE. + // uint32_t n0inv; // Precomputed montgomery parameter: -1 / n[0] mod 2^32 + // uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; // RSA modulus as a little-endian array. + // uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // Montgomery parameter R^2 as a little-endian array. + // uint32_t exponent; // RSA modulus: 3 or 65537 + // } RSAPublicKey; + + /** + * Allocates a new {@link RSAPublicKey} object, decodes a public RSA key stored in Android's custom binary format, + * and sets the key parameters. The resulting key can be used with the standard Java cryptography API to perform + * public operations. + * + * @param androidPubkey Public RSA key in Android's custom binary format. The size of the key must be at least + * {@link #ANDROID_PUBKEY_ENCODED_SIZE} + * @return {@link RSAPublicKey} object + */ + @NonNull + public static RSAPublicKey decode(@NonNull byte[] androidPubkey) + throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException { + BigInteger n; + BigInteger e; + + // Check size is large enough and the modulus size is correct. + if (androidPubkey.length < ANDROID_PUBKEY_ENCODED_SIZE) { + throw new InvalidKeyException("Invalid key length"); + } + ByteBuffer keyStruct = ByteBuffer.wrap(androidPubkey).order(ByteOrder.LITTLE_ENDIAN); + int modulusSize = keyStruct.getInt(); + if (modulusSize != ANDROID_PUBKEY_MODULUS_SIZE_WORDS) { + throw new InvalidKeyException("Invalid modulus length."); + } + + // Convert the modulus to big-endian byte order as expected by BN_bin2bn. + byte[] modulus = new byte[ANDROID_PUBKEY_MODULUS_SIZE]; + keyStruct.position(8); + keyStruct.get(modulus); + n = new BigInteger(1, swapEndianness(modulus)); + + // Read the exponent. + keyStruct.position(520); + e = BigInteger.valueOf(keyStruct.getInt()); + + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); + return (RSAPublicKey) keyFactory.generatePublic(publicKeySpec); + } + + /** + * Encodes the given key in the Android RSA public key binary format. + * + * @return Public RSA key in Android's custom binary format. The size of the key should be at least + * {@link #ANDROID_PUBKEY_ENCODED_SIZE} + */ + @NonNull + public static byte[] encode(@NonNull RSAPublicKey publicKey) throws InvalidKeyException { + BigInteger r32; + BigInteger n0inv; + BigInteger rr; + + if (publicKey.getModulus().toByteArray().length < ANDROID_PUBKEY_MODULUS_SIZE) { + throw new InvalidKeyException("Invalid key length " + publicKey.getModulus().toByteArray().length); + } + + ByteBuffer keyStruct = ByteBuffer.allocate(ANDROID_PUBKEY_ENCODED_SIZE).order(ByteOrder.LITTLE_ENDIAN); + // Store the modulus size. + keyStruct.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS); // modulus_size_words + + // Compute and store n0inv = -1 / N[0] mod 2^32. + r32 = BigInteger.ZERO.setBit(32); // r32 = 2^32 + n0inv = publicKey.getModulus().mod(r32); // n0inv = N[0] mod 2^32 + n0inv = n0inv.modInverse(r32); // n0inv = 1/n0inv mod 2^32 + n0inv = r32.subtract(n0inv); // n0inv = 2^32 - n0inv + keyStruct.putInt(n0inv.intValue()); // n0inv + + // Store the modulus. + keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, publicKey.getModulus()))); + + // Compute and store rr = (2^(rsa_size)) ^ 2 mod N. + rr = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8); // rr = 2^(rsa_size) + rr = rr.modPow(BigInteger.valueOf(2), publicKey.getModulus()); // rr = rr^2 mod N + keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, rr))); + + // Store the exponent. + keyStruct.putInt(publicKey.getPublicExponent().intValue()); // exponent + + return keyStruct.array(); + } + + @Nullable + private static byte[] BigEndianToLittleEndianPadded(int len, @NonNull BigInteger in) { + byte[] out = new byte[len]; + byte[] bytes = swapEndianness(in.toByteArray()); // Convert big endian -> little endian + int num_bytes = bytes.length; + if (len < num_bytes) { + if (!fitsInBytes(bytes, num_bytes, len)) { + return null; + } + num_bytes = len; + } + System.arraycopy(bytes, 0, out, 0, num_bytes); + return out; + } + + static boolean fitsInBytes(@NonNull byte[] bytes, int num_bytes, int len) { + byte mask = 0; + for (int i = len; i < num_bytes; i++) { + mask |= bytes[i]; + } + return mask == 0; + } + + @NonNull + private static byte[] swapEndianness(@NonNull byte[] bytes) { + int len = bytes.length; + byte[] out = new byte[len]; + for (int i = 0; i < len; ++i) { + out[i] = bytes[len - i - 1]; + } + return out; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java new file mode 100644 index 0000000..030437a --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import java.io.ByteArrayOutputStream; + +class ByteArrayNoThrowOutputStream extends ByteArrayOutputStream { + public ByteArrayNoThrowOutputStream() { + super(); + } + + public ByteArrayNoThrowOutputStream(int size) { + super(size); + } + + @Override + public void write(byte[] b) { + write(b, 0, b.length); + } + + @Override + public void close() { + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java new file mode 100644 index 0000000..97a81c1 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; + +import javax.security.auth.DestroyFailedException; + +final class KeyPair { + private final PrivateKey mPrivateKey; + private final Certificate mCertificate; + + public KeyPair(PrivateKey privateKey, Certificate certificate) { + mPrivateKey = privateKey; + mCertificate = certificate; + } + + public PrivateKey getPrivateKey() { + return mPrivateKey; + } + + public PublicKey getPublicKey() { + return mCertificate.getPublicKey(); + } + + public Certificate getCertificate() { + return mCertificate; + } + + public void destroy() throws DestroyFailedException { + try { + mPrivateKey.destroy(); + } catch (NoSuchMethodError ignore) { + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java new file mode 100644 index 0000000..4dac75f --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java @@ -0,0 +1,271 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.text.TextUtils; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + + +/** + * Local services extracted from the ADB client + * for easy access. + */ +public class LocalServices { + static final int SERVICE_FIRST = 1; + + public static final int SHELL = 1; + /** + * Remount the device's filesystem in read-write mode, instead of read-only. This is usually necessary before + * performing an {@link #SYNC} request. This request may not succeed on certain builds which do not allow that. + *

+ * This essentially executes {@code /system/bin/remount} command. Additional arguments such as {@code -R} can be + * passed too. + */ + public static final int REMOUNT = 2; + public static final int FILE = 3; + public static final int TCP_CONNECT = 4; + public static final int LOCAL_UNIX_SOCKET = 5; + public static final int LOCAL_UNIX_SOCKET_RESERVED = 6; + public static final int LOCAL_UNIX_SOCKET_ABSTRACT = 7; + public static final int LOCAL_UNIX_SOCKET_FILE_SYSTEM = 8; + /** + * Receive snapshots of the framebuffer. It requires sufficient privileges (or the connection is closed immediately) + * but works as follows: + *

+ * After an {@link AdbStream} is opened, ADB daemon sends a 16-byte binary structure containing the following fields + * (little-endian format): + *

+     * uint32_t depth;     // framebuffer depth = 16
+     * uint32_t size;      // framebuffer size in bytes = 2 * width * height
+     * uint32_t width;     // framebuffer width in pixels
+     * uint32_t height;    // framebuffer height in pixels
+     * 
+ * After that, each time a snapshot is wanted, one byte should be sent through the channel, which will trigger the + * daemon to send {@code size} bytes of framebuffer data. + */ + public static final int FRAMEBUFFER = 9; + /** + * Connects to the JDWP thread running in the VM of process PID (specified as an argument). + */ + public static final int CONNECT_JDWP = 10; + /** + * Receive the list of JDWP PIDs periodically. The format of the returned data is the following (in order): + *
    + *
  1. {@code hex4}: The length of all content as a 4-char hexadecimal string i.e. {@code %04zx}. + *
  2. {@code content}: A series of ASCII lines of the following format: + *
    +     *  <pid> "\n"
    +     *  
    + *
+ * This service is used by DDMS to know which debuggable processes are running on the device/emulator. + *

+ * Note that there is no single-shot service to retrieve the list only once. + */ + public static final int TRACK_JDWP = 11; + public static final int SYNC = 12; + /** + * Reverse socket connections from the device running ADB daemon to this client. This should not be used if both + * the ADB daemon and the client are in the same device. + *

+ * It takes an additional argument called {@code forward-command}. It can be one of the following: + *

    + *
  • {@code list-forward}: List all forwarded connections from the device + * This returns something that looks like the following: + *
      + *
    1. {@code hex4}: The length of the payload, as 4 hexadecimal chars i.e. {@code %04zx}. + *
    2. {@code payload}: A series of lines of the following format: + *
      +     *     host " " <local> " " <remote> "\n"
      +     *     
      + * Where <local> is the device-specific endpoint (e.g. {@code tcp:9000}), and <remote> is the + * client-specific endpoint. + *
    + *
  • forward:; + *
  • forward:norebind:; + *
  • killforward-all + *
  • killforward: + *
+ */ + public static final int REVERSE = 13; + /** + * Backup some or all packages installed in the device. For this to work, {@code allowBackup=true} must be present + * in the application section of the AndroidManifest.xml of the app. + *

+ * It takes additional arguments which can be one of the following: + *

    + *
  • List of packages (as array) + *
  • {@code -all} + *
  • {@code -shared} + *
+ * Output is a stream which is in zlib format with 24 bytes at the front (if unencrypted). + */ + public static final int BACKUP = 14; + /** + * Restore a backup. Input is a stream which is in zlib format with 24 bytes at the front (if unencrypted). + */ + public static final int RESTORE = 15; + + static final int SERVICE_LAST = 15; + + @IntDef({ + SHELL, + REMOUNT, + FILE, + TCP_CONNECT, + LOCAL_UNIX_SOCKET, + LOCAL_UNIX_SOCKET_RESERVED, + LOCAL_UNIX_SOCKET_ABSTRACT, + LOCAL_UNIX_SOCKET_FILE_SYSTEM, + FRAMEBUFFER, + CONNECT_JDWP, + TRACK_JDWP, + SYNC, + REVERSE, + BACKUP, + RESTORE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Services { + } + + @NonNull + static String getServiceName(@Services int service) { + switch (service) { + case SHELL: + return "shell:"; + case CONNECT_JDWP: + return "jdwp:"; + case FILE: + return "dev:"; + case FRAMEBUFFER: + return "framebuffer:"; + case LOCAL_UNIX_SOCKET: + return "local:"; + case LOCAL_UNIX_SOCKET_ABSTRACT: + return "localabstract:"; + case LOCAL_UNIX_SOCKET_FILE_SYSTEM: + return "localfilesystem:"; + case LOCAL_UNIX_SOCKET_RESERVED: + return "localreserved:"; + case REMOUNT: + return "remount:"; + case REVERSE: + return "reverse:"; + case SYNC: + return "sync:"; + case TCP_CONNECT: + return "tcp:"; + case TRACK_JDWP: + return "track-jdwp"; + case BACKUP: + return "backup:"; + case RESTORE: + return "restore:"; + default: + throw new IllegalArgumentException("Invalid service: " + service); + } + } + + @NonNull + static String getDestination(@Services int service, @NonNull String... args) { + String serviceName = getServiceName(service); + StringBuilder destination = new StringBuilder(serviceName); + switch (service) { + case SHELL: + for (String arg : args) { + if (arg.contains("\"")) { + throw new IllegalArgumentException("Arguments for inline shell cannot contain double" + + " quotations."); + } + if (arg.contains(" ")) { + destination.append("\"").append(Objects.requireNonNull(arg)).append("\""); + } else destination.append(Objects.requireNonNull(arg)); + } + break; + case FILE: + if (args.length == 0) { + throw new IllegalArgumentException("File name must be specified."); + } else if (args.length != 1) { + throw new IllegalArgumentException("Service expects exactly one argument, " + args.length + + " supplied."); + } + destination.append(Objects.requireNonNull(args[0])); + break; + case TCP_CONNECT: + if (args.length == 0) { + throw new IllegalArgumentException("Port number must be specified."); + } else if (args.length == 1) { + destination.append(args[0]); + } else if (args.length == 2) { + destination.append(Objects.requireNonNull(args[0])) + .append(':') + .append(Objects.requireNonNull(args[1])); + } else { + throw new IllegalArgumentException("Invalid number of arguments supplied."); + } + break; + case LOCAL_UNIX_SOCKET: + case LOCAL_UNIX_SOCKET_ABSTRACT: + case LOCAL_UNIX_SOCKET_FILE_SYSTEM: + case LOCAL_UNIX_SOCKET_RESERVED: + if (args.length == 0) { + throw new IllegalArgumentException("Path must be specified."); + } else if (args.length != 1) { + throw new IllegalArgumentException("Service expects exactly one argument, " + args.length + + " supplied."); + } + destination.append(Objects.requireNonNull(args[0])); + break; + case CONNECT_JDWP: + if (args.length == 0) { + throw new IllegalArgumentException("PID must be specified."); + } else if (args.length != 1) { + throw new IllegalArgumentException("Service expects exactly one argument, " + args.length + + " supplied."); + } + destination.append(Objects.requireNonNull(args[0])); + break; + case REVERSE: + if (args.length == 0) { + throw new IllegalArgumentException("Forward command must be specified."); + } else if (args.length != 1) { + throw new IllegalArgumentException("Service expects exactly one argument, " + args.length + + " supplied."); + } + if (args[0] == null) { + throw new IllegalArgumentException("Forward command is empty"); + } + if ("list-forward".equals(args[0]) || "killforward-all".equals(args[0])) { + destination.append(args[0]); + } else if (args[0].startsWith("forward:") || args[0].startsWith("killforward:")) { + destination.append(args[0]); + } else { + throw new IllegalArgumentException("Invalid forward command."); + } + break; + case BACKUP: + if (args.length == 0) { + throw new IllegalArgumentException("At least one package must be specified or use -shared/-all."); + } + case REMOUNT: + // Additional arguments for the commands + destination.append(TextUtils.join(" ", args)); + break; + case RESTORE: + case FRAMEBUFFER: + case SYNC: + case TRACK_JDWP: + if (args.length != 0) { + throw new IllegalArgumentException("Service expects no arguments."); + } + break; + } + return destination.toString(); + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java new file mode 100644 index 0000000..e7fc0cf --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java @@ -0,0 +1,319 @@ +// SPDX-License-Identifier: MIT AND (GPL-3.0-or-later OR Apache-2.0) + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.os.Build; +import android.os.Process; +import android.util.Log; + +import androidx.annotation.GuardedBy; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.security.NoSuchAlgorithmException; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.SecureRandomSpi; +import java.security.Security; + +/** + * Fixes for the output of the default PRNG having low entropy. + *

+ * The fixes need to be applied via {@link #apply()} before any use of Java + * Cryptography Architecture primitives. A good place to invoke them is in the + * application's {@code onCreate}. + */ +// Copyright 2013 Google Inc. +public final class PRNGFixes { + private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); + + /** + * Hidden constructor to prevent instantiation. + */ + private PRNGFixes() { + } + + /** + * Applies all fixes. + * + * @throws SecurityException if a fix is needed but could not be applied. + */ + public static void apply() { + applyOpenSSLFix(); + installLinuxPRNGSecureRandom(); + } + + /** + * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the + * fix is not needed. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void applyOpenSSLFix() throws SecurityException { + if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) + || (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) { + // No need to apply the fix + return; + } + + try { + // Mix in the device- and invocation-specific seed. + Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_seed", byte[].class) + .invoke(null, generateSeed()); + + // Mix output of Linux PRNG into OpenSSL's PRNG + int bytesRead = (Integer) Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") + .getMethod("RAND_load_file", String.class, long.class) + .invoke(null, "/dev/urandom", 1024); + if (bytesRead != 1024) { + throw new IOException("Unexpected number of bytes read from Linux PRNG: " + bytesRead); + } + } catch (Exception e) { + throw new SecurityException("Failed to seed OpenSSL PRNG", e); + } + } + + /** + * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the + * default. Does nothing if the implementation is already the default or if + * there is not need to install the implementation. + * + * @throws SecurityException if the fix is needed but could not be applied. + */ + private static void installLinuxPRNGSecureRandom() + throws SecurityException { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { + // No need to apply the fix + return; + } + + // Install a Linux PRNG-based SecureRandom implementation as the + // default, if not yet installed. + Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG"); + if ((secureRandomProviders == null) + || (secureRandomProviders.length < 1) + || (!LinuxPRNGSecureRandomProvider.class.equals(secureRandomProviders[0].getClass()))) { + Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); + } + + // Assert that new SecureRandom() and + // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed + // by the Linux PRNG-based SecureRandom implementation. + SecureRandom rng1 = new SecureRandom(); + if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider().getClass())) { + throw new SecurityException("new SecureRandom() backed by wrong Provider: " + + rng1.getProvider().getClass()); + } + + SecureRandom rng2; + try { + rng2 = SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new SecurityException("SHA1PRNG not available", e); + } + if (!LinuxPRNGSecureRandomProvider.class.equals( + rng2.getProvider().getClass())) { + throw new SecurityException("SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong provider: " + + rng2.getProvider().getClass()); + } + } + + /** + * {@code Provider} of {@code SecureRandom} engines which pass through + * all requests to the Linux PRNG. + */ + private static class LinuxPRNGSecureRandomProvider extends Provider { + + public LinuxPRNGSecureRandomProvider() { + super("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses /dev/urandom"); + // Although /dev/urandom is not a SHA-1 PRNG, some apps + // explicitly request a SHA1PRNG SecureRandom and we thus need to + // prevent them from getting the default implementation whose output + // may have low entropy. + put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); + put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); + } + } + + /** + * {@link SecureRandomSpi} which passes all requests to the Linux PRNG + * ({@code /dev/urandom}). + */ + public static class LinuxPRNGSecureRandom extends SecureRandomSpi { + + /* + * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed + * are passed through to the Linux PRNG (/dev/urandom). Instances of + * this class seed themselves by mixing in the current time, PID, UID, + * build fingerprint, and hardware serial number (where available) into + * Linux PRNG. + * + * Concurrency: Read requests to the underlying Linux PRNG are + * serialized (on sLock) to ensure that multiple threads do not get + * duplicated PRNG output. + */ + + private static final File URANDOM_FILE = new File("/dev/urandom"); + + private static final Object sLock = new Object(); + + /** + * Input stream for reading from Linux PRNG or {@code null} if not yet + * opened. + */ + @GuardedBy("sLock") + private static DataInputStream sUrandomIn; + + /** + * Output stream for writing to Linux PRNG or {@code null} if not yet + * opened. + */ + @GuardedBy("sLock") + private static OutputStream sUrandomOut; + + /** + * Whether this engine instance has been seeded. This is needed because + * each instance needs to seed itself if the client does not explicitly + * seed it. + */ + private boolean mSeeded; + + @Override + protected void engineSetSeed(byte[] bytes) { + try { + OutputStream out; + synchronized (sLock) { + out = getUrandomOutputStream(); + } + out.write(bytes); + out.flush(); + } catch (IOException e) { + // On a small fraction of devices /dev/urandom is not writable. + // Log and ignore. + Log.w(PRNGFixes.class.getSimpleName(), + "Failed to mix seed into " + URANDOM_FILE); + } finally { + mSeeded = true; + } + } + + @Override + protected void engineNextBytes(byte[] bytes) { + if (!mSeeded) { + // Mix in the device- and invocation-specific seed. + engineSetSeed(generateSeed()); + } + + try { + DataInputStream in; + synchronized (sLock) { + in = getUrandomInputStream(); + } + synchronized (in) { + in.readFully(bytes); + } + } catch (IOException e) { + throw new SecurityException( + "Failed to read from " + URANDOM_FILE, e); + } + } + + @Override + protected byte[] engineGenerateSeed(int size) { + byte[] seed = new byte[size]; + engineNextBytes(seed); + return seed; + } + + private DataInputStream getUrandomInputStream() { + synchronized (sLock) { + if (sUrandomIn == null) { + // NOTE: Consider inserting a BufferedInputStream between + // DataInputStream and FileInputStream if you need higher + // PRNG output performance and can live with future PRNG + // output being pulled into this process prematurely. + try { + sUrandomIn = new DataInputStream( + new FileInputStream(URANDOM_FILE)); + } catch (IOException e) { + throw new SecurityException("Failed to open " + + URANDOM_FILE + " for reading", e); + } + } + return sUrandomIn; + } + } + + private OutputStream getUrandomOutputStream() throws IOException { + synchronized (sLock) { + if (sUrandomOut == null) { + sUrandomOut = new FileOutputStream(URANDOM_FILE); + } + return sUrandomOut; + } + } + } + + /** + * Generates a device- and invocation-specific seed to be mixed into the + * Linux PRNG. + */ + private static byte[] generateSeed() { + try { + ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); + DataOutputStream seedBufferOut = + new DataOutputStream(seedBuffer); + seedBufferOut.writeLong(System.currentTimeMillis()); + seedBufferOut.writeLong(System.nanoTime()); + seedBufferOut.writeInt(Process.myPid()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BASE_1_1) { + seedBufferOut.writeInt(Process.myUid()); + } + seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); + seedBufferOut.close(); + return seedBuffer.toByteArray(); + } catch (IOException e) { + throw new SecurityException("Failed to generate seed", e); + } + } + + /** + * Gets the hardware serial number of this device. + * + * @return serial number or {@code null} if not available. + */ + private static String getDeviceSerialNumber() { + // We're using the Reflection API because Build.SERIAL is only available + // since API Level 9 (Gingerbread, Android 2.3). + try { + return (String) Build.class.getField("SERIAL").get(null); + } catch (Exception ignored) { + return null; + } + } + + private static byte[] getBuildFingerprintAndDeviceSerial() { + StringBuilder result = new StringBuilder(); + String fingerprint = Build.FINGERPRINT; + if (fingerprint != null) { + result.append(fingerprint); + } + String serial = getDeviceSerialNumber(); + if (serial != null) { + result.append(serial); + } + try { + return result.toString().getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("UTF-8 encoding not supported"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java new file mode 100644 index 0000000..e1ea3f2 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.VisibleForTesting; + +import org.bouncycastle.crypto.InvalidCipherTextException; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.generators.HKDFBytesGenerator; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.modes.GCMModeCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.HKDFParameters; +import org.bouncycastle.crypto.params.KeyParameter; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +import javax.security.auth.Destroyable; + +import io.github.muntashirakon.crypto.spake2.Spake2Context; +import io.github.muntashirakon.crypto.spake2.Spake2Role; + + +@RequiresApi(Build.VERSION_CODES.GINGERBREAD) +class PairingAuthCtx implements Destroyable { + // The following values are taken from the following source and are subjected to change + // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/pairing_auth.cpp + private static final byte[] CLIENT_NAME = StringCompat.getBytes("adb pair client\u0000", "UTF-8"); + private static final byte[] SERVER_NAME = StringCompat.getBytes("adb pair server\u0000", "UTF-8"); + + // The following values are taken from the following source and are subjected to change + // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/aes_128_gcm.cpp + private static final byte[] INFO = StringCompat.getBytes("adb pairing_auth aes-128-gcm key", "UTF-8"); + private static final int HKDF_KEY_LENGTH = 128 / 8; + public static final int GCM_IV_LENGTH = 12; // in bytes + + private final byte[] mMsg; + private final Spake2Context mSpake2Ctx; + private final byte[] mSecretKey = new byte[HKDF_KEY_LENGTH]; + private long mDecIv = 0; + private long mEncIv = 0; + private boolean mIsDestroyed = false; + + @Nullable + public static PairingAuthCtx createAlice(byte[] password) { + Spake2Context spake25519 = new Spake2Context(Spake2Role.Alice, CLIENT_NAME, SERVER_NAME); + try { + return new PairingAuthCtx(spake25519, password); + } catch (IllegalArgumentException | IllegalStateException e) { + return null; + } + } + + @VisibleForTesting + @Nullable + public static PairingAuthCtx createBob(byte[] password) { + Spake2Context spake25519 = new Spake2Context(Spake2Role.Bob, SERVER_NAME, CLIENT_NAME); + try { + return new PairingAuthCtx(spake25519, password); + } catch (IllegalArgumentException | IllegalStateException e) { + return null; + } + } + + private PairingAuthCtx(Spake2Context spake25519, byte[] password) + throws IllegalArgumentException, IllegalStateException { + mSpake2Ctx = spake25519; + mMsg = mSpake2Ctx.generateMessage(password); + } + + public byte[] getMsg() { + return mMsg; + } + + public boolean initCipher(byte[] theirMsg) throws IllegalArgumentException, IllegalStateException { + if (mIsDestroyed) return false; + byte[] keyMaterial = mSpake2Ctx.processMessage(theirMsg); + if (keyMaterial == null) return false; + HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest()); + hkdf.init(new HKDFParameters(keyMaterial, null, INFO)); + hkdf.generateBytes(mSecretKey, 0, mSecretKey.length); + return true; + } + + @Nullable + public byte[] encrypt(@NonNull byte[] in) { + return encryptDecrypt(true, in, ByteBuffer.allocate(GCM_IV_LENGTH) + .order(ByteOrder.LITTLE_ENDIAN).putLong(mEncIv++).array()); + } + + @Nullable + public byte[] decrypt(@NonNull byte[] in) { + return encryptDecrypt(false, in, ByteBuffer.allocate(GCM_IV_LENGTH) + .order(ByteOrder.LITTLE_ENDIAN).putLong(mDecIv++).array()); + } + + @Override + public boolean isDestroyed() { + return mIsDestroyed; + } + + @Override + public void destroy() { + mIsDestroyed = true; + Arrays.fill(mSecretKey, (byte) 0); + mSpake2Ctx.destroy(); + } + + @Nullable + private byte[] encryptDecrypt(boolean forEncryption, @NonNull byte[] in, @NonNull byte[] iv) { + if (mIsDestroyed) return null; + AEADParameters spec = new AEADParameters(new KeyParameter(mSecretKey), mSecretKey.length * 8, iv); + GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); + cipher.init(forEncryption, spec); + byte[] out = new byte[cipher.getOutputSize(in.length)]; + int newOffset = cipher.processBytes(in, 0, in.length, out, 0); + try { + cipher.doFinal(out, newOffset); + } catch (InvalidCipherTextException e) { + return null; + } + return out; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java new file mode 100644 index 0000000..f04883d --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.annotation.SuppressLint; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.io.Closeable; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.Socket; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.interfaces.RSAPublicKey; +import java.util.Arrays; +import java.util.Objects; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLSocket; + +// https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_connection/pairing_connection.cpp +// Also based on Shizuku's implementation +@RequiresApi(Build.VERSION_CODES.GINGERBREAD) +public final class PairingConnectionCtx implements Closeable { + public static final String TAG = PairingConnectionCtx.class.getSimpleName(); + + public static final String EXPORTED_KEY_LABEL = "adb-label\u0000"; + public static final int EXPORT_KEY_SIZE = 64; + + private enum State { + Ready, + ExchangingMsgs, + ExchangingPeerInfo, + Stopped + } + + enum Role { + Client, + Server, + } + + private final String mHost; + private final int mPort; + private final byte[] mPswd; + private final PeerInfo mPeerInfo; + private final SSLContext mSslContext; + private final Role mRole = Role.Client; + + private DataInputStream mInputStream; + private DataOutputStream mOutputStream; + private PairingAuthCtx mPairingAuthCtx; + private State mState = State.Ready; + + public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull KeyPair keyPair, + @NonNull String deviceName) + throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException { + this.mHost = Objects.requireNonNull(host); + this.mPort = port; + this.mPswd = Objects.requireNonNull(pswd); + this.mPeerInfo = new PeerInfo(PeerInfo.ADB_RSA_PUB_KEY, AndroidPubkey.encodeWithName((RSAPublicKey) + keyPair.getPublicKey(), Objects.requireNonNull(deviceName))); + this.mSslContext = SslUtils.getSslContext(keyPair); + } + + public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull PrivateKey privateKey, + @NonNull Certificate certificate, @NonNull String deviceName) + throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException { + this(host, port, pswd, new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)), + deviceName); + } + + public void start() throws IOException { + if (mState != State.Ready) { + throw new IOException("Connection is not ready yet."); + } + + mState = State.ExchangingMsgs; + + // Start worker + setupTlsConnection(); + + for (; ; ) { + switch (mState) { + case ExchangingMsgs: + if (!doExchangeMsgs()) { + notifyResult(); + throw new IOException("Exchanging message wasn't successful."); + } + mState = State.ExchangingPeerInfo; + break; + case ExchangingPeerInfo: + if (!doExchangePeerInfo()) { + notifyResult(); + throw new IOException("Could not exchange peer info."); + } + notifyResult(); + return; + case Ready: + case Stopped: + throw new IOException("Connection closed with errors."); + } + } + } + + private void notifyResult() { + mState = State.Stopped; + } + + private void setupTlsConnection() throws IOException { + Socket socket; + if (mRole == Role.Server) { + SSLServerSocket sslServerSocket = (SSLServerSocket) mSslContext.getServerSocketFactory().createServerSocket(mPort); + socket = sslServerSocket.accept(); + // TODO: Write automated test scripts after removing Conscrypt dependency. + } else { // role == Role.Client + socket = new Socket(mHost, mPort); + } + socket.setTcpNoDelay(true); + + // We use custom SSLContext to allow any SSL certificates + SSLSocket sslSocket = (SSLSocket) mSslContext.getSocketFactory().createSocket(socket, mHost, mPort, true); + sslSocket.startHandshake(); + Log.d(TAG, "Handshake succeeded."); + + mInputStream = new DataInputStream(sslSocket.getInputStream()); + mOutputStream = new DataOutputStream(sslSocket.getOutputStream()); + + // To ensure the connection is not stolen while we do the PAKE, append the exported key material from the + // tls connection to the password. + byte[] keyMaterial = exportKeyingMaterial(sslSocket, EXPORT_KEY_SIZE); + byte[] passwordBytes = new byte[mPswd.length + keyMaterial.length]; + System.arraycopy(mPswd, 0, passwordBytes, 0, mPswd.length); + System.arraycopy(keyMaterial, 0, passwordBytes, mPswd.length, keyMaterial.length); + + PairingAuthCtx pairingAuthCtx = PairingAuthCtx.createAlice(passwordBytes); + if (pairingAuthCtx == null) { + throw new IOException("Unable to create PairingAuthCtx."); + } + this.mPairingAuthCtx = pairingAuthCtx; + } + + @SuppressLint("PrivateApi") // Conscrypt is a stable private API + private byte[] exportKeyingMaterial(SSLSocket sslSocket, int length) throws SSLException { + // Conscrypt#exportKeyingMaterial(SSLSocket socket, String label, byte[] context, int length): byte[] + // throws SSLException + try { + Class conscryptClass; + if (SslUtils.isCustomConscrypt()) { + conscryptClass = Class.forName("org.conscrypt.Conscrypt"); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // Although support for conscrypt has been added in Android 5.0 (Lollipop), + // TLS1.3 isn't supported until Android 9 (Pie). + throw new SSLException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead."); + } else { + conscryptClass = Class.forName("com.android.org.conscrypt.Conscrypt"); + } + Method exportKeyingMaterial = conscryptClass.getMethod("exportKeyingMaterial", SSLSocket.class, + String.class, byte[].class, int.class); + return (byte[]) exportKeyingMaterial.invoke(null, sslSocket, EXPORTED_KEY_LABEL, null, length); + } catch (SSLException e) { + throw e; + } catch (Throwable th) { + throw new SSLException(th); + } + } + + private void writeHeader(@NonNull PairingPacketHeader header, @NonNull byte[] payload) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE) + .order(ByteOrder.BIG_ENDIAN); + header.writeTo(buffer); + + mOutputStream.write(buffer.array()); + mOutputStream.write(payload); + } + + @Nullable + private PairingPacketHeader readHeader() throws IOException { + byte[] bytes = new byte[PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE]; + mInputStream.readFully(bytes); + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); + return PairingPacketHeader.readFrom(buffer); + } + + @NonNull + private PairingPacketHeader createHeader(byte type, int payloadSize) { + return new PairingPacketHeader(PairingPacketHeader.CURRENT_KEY_HEADER_VERSION, type, payloadSize); + } + + private boolean checkHeaderType(byte expected, byte actual) { + if (expected != actual) { + Log.e(TAG, "Unexpected header type (expected=" + expected + " actual=" + actual + ")"); + return false; + } + return true; + } + + private boolean doExchangeMsgs() throws IOException { + byte[] msg = mPairingAuthCtx.getMsg(); + + PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.SPAKE2_MSG, msg.length); + // Write our SPAKE2 msg + writeHeader(ourHeader, msg); + + // Read the peer's SPAKE2 msg header + PairingPacketHeader theirHeader = readHeader(); + if (theirHeader == null || !checkHeaderType(PairingPacketHeader.SPAKE2_MSG, theirHeader.type)) return false; + + // Read the SPAKE2 msg payload and initialize the cipher for encrypting the PeerInfo and certificate. + byte[] theirMsg = new byte[theirHeader.payloadSize]; + mInputStream.readFully(theirMsg); + + try { + return mPairingAuthCtx.initCipher(theirMsg); + } catch (Exception e) { + Log.e(TAG, "Unable to initialize pairing cipher"); + //noinspection UnnecessaryInitCause + throw (IOException) new IOException().initCause(e); + } + } + + private boolean doExchangePeerInfo() throws IOException { + // Encrypt PeerInfo + ByteBuffer buffer = ByteBuffer.allocate(PeerInfo.MAX_PEER_INFO_SIZE).order(ByteOrder.BIG_ENDIAN); + mPeerInfo.writeTo(buffer); + byte[] outBuffer = mPairingAuthCtx.encrypt(buffer.array()); + if (outBuffer == null) { + Log.e(TAG, "Failed to encrypt peer info"); + return false; + } + + // Write out the packet header + PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.PEER_INFO, outBuffer.length); + // Write out the encrypted payload + writeHeader(ourHeader, outBuffer); + + // Read in the peer's packet header + PairingPacketHeader theirHeader = readHeader(); + if (theirHeader == null || !checkHeaderType(PairingPacketHeader.PEER_INFO, theirHeader.type)) return false; + + // Read in the encrypted peer certificate + byte[] theirMsg = new byte[theirHeader.payloadSize]; + mInputStream.readFully(theirMsg); + + // Try to decrypt the certificate + byte[] decryptedMsg = mPairingAuthCtx.decrypt(theirMsg); + if (decryptedMsg == null) { + Log.e(TAG, "Unsupported payload while decrypting peer info."); + return false; + } + + // The decrypted message should contain the PeerInfo. + if (decryptedMsg.length != PeerInfo.MAX_PEER_INFO_SIZE) { + Log.e(TAG, "Got size=" + decryptedMsg.length + " PeerInfo.size=" + PeerInfo.MAX_PEER_INFO_SIZE); + return false; + } + + PeerInfo theirPeerInfo = PeerInfo.readFrom(ByteBuffer.wrap(decryptedMsg)); + Log.d(TAG, theirPeerInfo.toString()); + return true; + } + + @Override + public void close() { + Arrays.fill(mPswd, (byte) 0); + try { + mInputStream.close(); + } catch (IOException ignore) { + } + try { + mOutputStream.close(); + } catch (IOException ignore) { + } + if (mState != State.Ready) { + mPairingAuthCtx.destroy(); + } + } + + private static class PeerInfo { + public static final int MAX_PEER_INFO_SIZE = 1 << 13; + + public static final byte ADB_RSA_PUB_KEY = 0; + public static final byte ADB_DEVICE_GUID = 0; + + @NonNull + public static PeerInfo readFrom(@NonNull ByteBuffer buffer) { + byte type = buffer.get(); + byte[] data = new byte[MAX_PEER_INFO_SIZE - 1]; + buffer.get(data); + return new PeerInfo(type, data); + } + + private final byte type; + private final byte[] data = new byte[MAX_PEER_INFO_SIZE - 1]; + + public PeerInfo(byte type, byte[] data) { + this.type = type; + System.arraycopy(data, 0, this.data, 0, Math.min(data.length, MAX_PEER_INFO_SIZE - 1)); + } + + public void writeTo(@NonNull ByteBuffer buffer) { + buffer.put(type).put(data); + } + + @NonNull + @Override + public String toString() { + return "PeerInfo{" + + "type=" + type + + ", data=" + Arrays.toString(data) + + '}'; + } + } + + private static class PairingPacketHeader { + public static final byte CURRENT_KEY_HEADER_VERSION = 1; + public static final byte MIN_SUPPORTED_KEY_HEADER_VERSION = 1; + public static final byte MAX_SUPPORTED_KEY_HEADER_VERSION = 1; + + public static final int MAX_PAYLOAD_SIZE = 2 * PeerInfo.MAX_PEER_INFO_SIZE; + public static final byte PAIRING_PACKET_HEADER_SIZE = 6; + + public static final byte SPAKE2_MSG = 0; + public static final byte PEER_INFO = 1; + + @Nullable + public static PairingPacketHeader readFrom(@NonNull ByteBuffer buffer) { + byte version = buffer.get(); + byte type = buffer.get(); + int payload = buffer.getInt(); + if (version < MIN_SUPPORTED_KEY_HEADER_VERSION || version > MAX_SUPPORTED_KEY_HEADER_VERSION) { + Log.e(TAG, "PairingPacketHeader version mismatch (us=" + CURRENT_KEY_HEADER_VERSION + + " them=" + version + ")"); + return null; + } + if (type != SPAKE2_MSG && type != PEER_INFO) { + Log.e(TAG, "Unknown PairingPacket type " + type); + return null; + } + if (payload <= 0 || payload > MAX_PAYLOAD_SIZE) { + Log.e(TAG, "Header payload not within a safe payload size (size=" + payload + ")"); + return null; + } + return new PairingPacketHeader(version, type, payload); + } + + private final byte version; + private final byte type; + private final int payloadSize; + + public PairingPacketHeader(byte version, byte type, int payloadSize) { + this.version = version; + this.type = type; + this.payloadSize = payloadSize; + } + + public void writeTo(@NonNull ByteBuffer buffer) { + buffer.put(version).put(type).putInt(payloadSize); + } + + @NonNull + @Override + public String toString() { + return "PairingPacketHeader{" + + "version=" + version + + ", type=" + type + + ", payloadSize=" + payloadSize + + '}'; + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java new file mode 100644 index 0000000..77f4dcb --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.annotation.SuppressLint; +import android.os.Build; + +import androidx.annotation.NonNull; + +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.Provider; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509TrustManager; + +final class SslUtils { + private static boolean customConscrypt = false; + private static SSLContext sslContext; + + public static boolean isCustomConscrypt() { + return customConscrypt; + } + + @SuppressLint("TrulyRandom") // The users are already instructed to fix this issue + @NonNull + public static SSLContext getSslContext(KeyPair keyPair) throws NoSuchAlgorithmException, KeyManagementException { + if (sslContext != null) { + return sslContext; + } + try { + Class providerClass = Class.forName("org.conscrypt.OpenSSLProvider"); + Provider openSslProvder = (Provider) providerClass.getDeclaredConstructor().newInstance(); + sslContext = SSLContext.getInstance("TLSv1.3", openSslProvder); + customConscrypt = true; + } catch (NoSuchAlgorithmException e) { + throw e; + } catch (Throwable e) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + // Custom error message to inform user that they should use custom Conscrypt library. + throw new NoSuchAlgorithmException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead."); + } + sslContext = SSLContext.getInstance("TLSv1.3"); + customConscrypt = false; + } + System.out.println("Using " + (customConscrypt ? "custom" : "default") + " TLSv1.3 provider..."); + sslContext.init(new KeyManager[]{getKeyManager(keyPair)}, + new X509TrustManager[]{getAllAcceptingTrustManager()}, + new SecureRandom()); + return sslContext; + } + + @NonNull + private static KeyManager getKeyManager(KeyPair keyPair) { + return new X509ExtendedKeyManager() { + private final String mAlias = "key"; + + @Override + public String[] getClientAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { + for (String keyType : keyTypes) { + if (keyType.equals("RSA")) return mAlias; + } + return null; + } + + @Override + public String[] getServerAliases(String keyType, Principal[] issuers) { + return null; + } + + @Override + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { + return null; + } + + @Override + public X509Certificate[] getCertificateChain(String alias) { + if (this.mAlias.equals(alias)) { + return new X509Certificate[]{(X509Certificate) keyPair.getCertificate()}; + } + return null; + } + + @Override + public PrivateKey getPrivateKey(String alias) { + if (this.mAlias.equals(alias)) { + return keyPair.getPrivateKey(); + } + return null; + } + }; + } + + @SuppressLint("TrustAllX509TrustManager") // Accept all certificates + @NonNull + private static X509TrustManager getAllAcceptingTrustManager() { + return new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java new file mode 100644 index 0000000..a51d8c4 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb; + +import android.os.Build; + +import androidx.annotation.NonNull; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.IllegalCharsetNameException; + +final class StringCompat { + @NonNull + public static byte[] getBytes(@NonNull String text, @NonNull String charsetName) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { + return text.getBytes(Charset.forName(charsetName)); + } + try { + return text.getBytes(charsetName); + } catch (UnsupportedEncodingException e) { + throw (IllegalCharsetNameException) new IllegalCharsetNameException("Illegal charset " + charsetName) + .initCause(e); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java new file mode 100644 index 0000000..a630c89 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android; + +import android.content.Context; +import android.net.nsd.NsdManager; +import android.net.nsd.NsdServiceInfo; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StringDef; + +import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.ServerSocket; +import java.net.SocketException; +import java.util.Collections; +import java.util.Objects; + +/** + * Automatic discovery of ADB daemons. + */ +// Copyright 2020 南宫雪珊 +// Copyright 2022 Muntashir Al-Islam +// Based on https://android.googlesource.com/platform/packages/modules/adb/+/eddd2d3a386a83f5d1e14f87a318adef4c2f1a9d/adb_mdns.cpp +@RequiresApi(Build.VERSION_CODES.JELLY_BEAN) +public class AdbMdns { + public static final String SERVICE_TYPE_ADB = "adb"; + public static final String SERVICE_TYPE_TLS_PAIRING = "adb-tls-pairing"; + public static final String SERVICE_TYPE_TLS_CONNECT = "adb-tls-connect"; + + @StringDef({ + SERVICE_TYPE_ADB, + SERVICE_TYPE_TLS_PAIRING, + SERVICE_TYPE_TLS_CONNECT, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ServiceType { + } + + public interface OnAdbDaemonDiscoveredListener { + void onPortChanged(@Nullable InetAddress hostAddress, int port); + } + + @NonNull + private final Context mContext; + @NonNull + private final String mServiceType; + @NonNull + private final OnAdbDaemonDiscoveredListener mAdbDaemonDiscoveredListener; + private final NsdManager.DiscoveryListener mDiscoveryListener; + private final NsdManager mNsdManager; + + private boolean mRegistered; + private boolean mRunning; + @Nullable + private String mServiceName; + + public AdbMdns(@NonNull Context context, @ServiceType @NonNull String serviceType, + @NonNull OnAdbDaemonDiscoveredListener portChangeListener) { + mContext = Objects.requireNonNull(context); + mServiceType = String.format("_%s._tcp", Objects.requireNonNull(serviceType)); + mAdbDaemonDiscoveredListener = Objects.requireNonNull(portChangeListener); + mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); + mDiscoveryListener = new DiscoveryListener(this); + } + + public void start() { + if (mRunning) return; + mRunning = true; + if (!mRegistered) { + mNsdManager.discoverServices(mServiceType, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); + } + } + + public void stop() { + if (!mRunning) return; + mRunning = false; + if (mRegistered) { + mNsdManager.stopServiceDiscovery(mDiscoveryListener); + } + } + + private void onDiscoveryStart() { + mRegistered = true; + } + + private void onDiscoverStop() { + mRegistered = false; + } + + private void onServiceFound(NsdServiceInfo serviceInfo) { + mNsdManager.resolveService(serviceInfo, new ResolveListener(this)); + } + + private void onServiceLost(NsdServiceInfo serviceInfo) { + if (mServiceName != null && mServiceName.equals(serviceInfo.getServiceName())) { + mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), -1); + } + } + + private void onServiceResolved(NsdServiceInfo serviceInfo) { + if (!mRunning) return; + try { + for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) { + for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { + String inetHost = inetAddress.getHostAddress(); + if (inetHost != null && inetHost.equals(serviceInfo.getHost().getHostAddress()) + && isPortAvailable(serviceInfo.getPort())) { + mServiceName = serviceInfo.getServiceName(); + mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), serviceInfo.getPort()); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + } + + private boolean isPortAvailable(int port) { + try (ServerSocket socket = new ServerSocket()) { + socket.bind(new InetSocketAddress(AndroidUtils.getHostIpAddress(mContext), port), 1); + return false; + } catch (IOException e) { + return true; + } + } + + private static class DiscoveryListener implements NsdManager.DiscoveryListener { + @NonNull + private final AdbMdns mAdbMdns; + + private DiscoveryListener(@NonNull AdbMdns adbMdns) { + mAdbMdns = adbMdns; + } + + @Override + public void onDiscoveryStarted(String serviceType) { + mAdbMdns.onDiscoveryStart(); + } + + @Override + public void onStartDiscoveryFailed(String serviceType, int errorCode) { + } + + @Override + public void onDiscoveryStopped(String serviceType) { + mAdbMdns.onDiscoverStop(); + } + + @Override + public void onStopDiscoveryFailed(String serviceType, int errorCode) { + } + + @Override + public void onServiceFound(NsdServiceInfo serviceInfo) { + mAdbMdns.onServiceFound(serviceInfo); + } + + @Override + public void onServiceLost(NsdServiceInfo serviceInfo) { + mAdbMdns.onServiceLost(serviceInfo); + } + } + + private static class ResolveListener implements NsdManager.ResolveListener { + @NonNull + private final AdbMdns mAdbMdns; + + private ResolveListener(@NonNull AdbMdns adbMdns) { + mAdbMdns = adbMdns; + } + + @Override + public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { + } + + @Override + public void onServiceResolved(NsdServiceInfo serviceInfo) { + mAdbMdns.onServiceResolved(serviceInfo); + } + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java new file mode 100644 index 0000000..8816934 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 + +package com.xypower.wpywapp.libadb.src.main.java.io.github.muntashirakon.adb.android; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Build; +import android.provider.Settings; + +import androidx.annotation.NonNull; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +public class AndroidUtils { + // https://github.com/firebase/firebase-android-sdk/blob/7d86138304a6573cbe2c61b66b247e930fa05767/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java#L402 + private static final String GOLDFISH = "goldfish"; + private static final String RANCHU = "ranchu"; + private static final String SDK = "sdk"; + + public static boolean isEmulator(@NonNull Context context) { + if (Build.PRODUCT.contains(SDK)) { + return true; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO + && (Build.HARDWARE.contains(GOLDFISH) || Build.HARDWARE.contains(RANCHU))) { + return true; + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { + @SuppressLint("HardwareIds") + String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + return androidId == null; + } + return false; + } + + + @NonNull + public static String getHostIpAddress(@NonNull Context context) { + if (AndroidUtils.isEmulator(context)) { + return "10.0.2.2"; + } + String ipAddress; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + ipAddress = InetAddress.getLoopbackAddress().getHostAddress(); + } else { + try { + ipAddress = InetAddress.getLocalHost().getHostAddress(); + } catch (UnknownHostException e) { + ipAddress = null; + } + } + if (ipAddress == null || ipAddress.equals("::1")) { + return "127.0.0.1"; + } + return ipAddress; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html new file mode 100644 index 0000000..704dd26 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/main/java/io/github/muntashirakon/adb/android/package.html @@ -0,0 +1 @@ +

All Android dependencies are kept under this package for easy reference.

\ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/libadb/src/test/java/io/github/muntashirakon/adb/AndroidPubkeyTest.java b/app/src/main/java/com/xypower/wpywapp/libadb/src/test/java/io/github/muntashirakon/adb/AndroidPubkeyTest.java new file mode 100644 index 0000000..ad35f28 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/libadb/src/test/java/io/github/muntashirakon/adb/AndroidPubkeyTest.java @@ -0,0 +1,118 @@ +//// SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 +// +//package com.xypower.wpywapp.libadb.src.test.java.io.github.muntashirakon.adb; +// +//import org.bouncycastle.util.encoders.Base64; +//import org.junit.Before; +//import org.junit.Test; +// +//import java.math.BigInteger; +//import java.security.InvalidKeyException; +//import java.security.NoSuchAlgorithmException; +//import java.security.Signature; +//import java.security.SignatureException; +//import java.security.interfaces.RSAPublicKey; +//import java.security.spec.InvalidKeySpecException; +// +//import static io.github.muntashirakon.adb.AndroidPubkey.ANDROID_PUBKEY_ENCODED_SIZE; +//import static org.junit.Assert.assertArrayEquals; +//import static org.junit.Assert.assertEquals; +//import static org.junit.Assert.assertTrue; +// +//public class AndroidPubkeyTest { +// private static final byte[] DIGEST = { +// 0x31, 0x5f, 0x5b, (byte) 0xdb, 0x76, (byte) 0xd0, 0x78, (byte) 0xc4, 0x3b, (byte) 0x8a, (byte) 0xc0, +// 0x06, 0x4e, 0x4a, 0x01, 0x64, 0x61, 0x2b, 0x1f, (byte) 0xce, 0x77, (byte) 0xc8, +// 0x69, 0x34, 0x5b, (byte) 0xfc, (byte) 0x94, (byte) 0xc7, 0x58, (byte) 0x94, (byte) 0xed, (byte) 0xd3, +// }; +// +// private static final String[] KEYS = new String[] { +// "20686253007643618731996444194404343867753606615063450617882673005393433941259733754887313620913867473991126076227684529071148116710971688307399269161393680066708223500416626973582453156074796463551655506880263757530692111705709492670974173865966567229986684076018218836054376260921024258822748029571012424062161309155451659011612585496480130049086533401220131725127467783734498206350584978609270260042864036755332689164701948323567009422782353582299103998793983781904622199786127904484293079447215050401933233201140660611995982950449318766441491470794461010754367235997277784582260919909111021889463573881293340673638258140979906632549357292907467007755298711750754693102503358563113321739768558800007075371722687735888311697725543005911629713945900921982291726988822299885412290333597947505237030964811359845572308590371290828162825800693816284267659154250460946638141531109051703958714241913036543711553816745407369784077289292046547482798618404863636910359464490602947766408857942988353632212055739706153132169632236839182513092231542800002960492871433639429992495815686493836203332828415821398124085709593551180637824827628263278195555177924931666478926276783820224389307415162160245683448074970045831874158357509710640316611667245663639463489328681291088128", +// "20686253017157961681070131581867072824582833493615686581285202812978323130952408474782337908483014193592959801158288937338861941384391935424181886115887706116180211910326203195827988548568848400075560545658694754072288281934799100824960003665469676627306218542649151650279594335310005774745074102233566034401681058875954456976451246992673780060830565203755535989743862421541016487655599803066890290569271073410408457924901854658523004566584730402488976324736395344746149490673499453625232592526755899773745566599094231998380788633185151420348626744906833795805930985207848484533950617753433631247755579325235753032891647748010767177734536686383017861046451119520379951845678816861944386294035286574657104145896837994175074910881605806540878194352563352369751154950231748036893872855863146059482454816118277619882879924798566063675418426269661488345544005535393697052314311247263890598514707311723166476167962362723441242864747039873917888833291982018201334656703016344691467362831812856230943332947764107530472082685276225170871545829843748520918630697076657160036883930216256677113853170428981812392602195530517909206428574912517211805856768824444514391062511206137932417723915871095033856634183547083377240616484404928845477796622124210865489291636384376815872", +// "20686253012016661596297187165295953810124840916410787780259125355974730724901079696472630368937851048257090320232652157100991308376039941481829068380105246658346925724672501337521327139011807932361821638555679532028068537874220234967686261347630150684757119393835764732734869517200772690364990583127671131537583714931243019173197423018607993303448083354399470192423616983206536107095715775647406269008431508946794838483788727203899942491271993309634327951609153908662621681729016801610515674359693917283475097384354342498810325041161909134028476434295385254073752420814778998269287749533881374904934126049438895801381045118590479396113271334390254276279559056023087328573647647215037607856427456918131285395255693120064580259556735556512377323876432429678739578757363573131334912599024688462135510569451655007983978712803921393726583392694890019375462303417604382964474778937531805444153682154861176782819373335110870826625829171651646773939431655533573703527937698849234129612444879578476798107659804571742743573317338460943463599693739087545180845307099319818906850017269705411337110447078163740825844263091960377332261204721538817750302929778362322626050195563553439929517852576974112801805532465084720065825179978157459297137449086759181900619029928059339008", +// "20686253007164298102187579022292645782622950267376457918101697669358007301488761837295142744126519186355305488389221986154518328646591249427359811715893827299671450881507028164629286041250862175310771677831707558127385367785197803483575096787182004608184320944234595728063837868041235349956855438493602107933402900375680275580658634153323631657275297128293064166637994755918951415154912946597772260559048829851194391800720841779417294685741008056484437199267285946970880090812268047882017049497063496655486104722944355404468824860822958344153152666873208144779124031791189998000506056940151806004196000409215856732235270100083588540707679527939391873638758465419555753702157561970233144942307459852882991188472466621152506161477788274973160814417916904823923993324434775898693916605927490225550209649971721585529738473244554435841443463085919856039187050589485138795602302083255665055214846777613999233572727226441658259782563053572457091354281339853515214717324207788284184709742610276702868576013972404040386528578798398612655692848123956484300353770097737367273673338660759524265042605762204830694090505973135478745543215676846804149964034093456987246386704478034104616628251724608616498482846759154973645939865112578396682953063481438609050519636632406065408", +// "20686253013631672077140860925430431681294767795873158523291037328668344494856968639956583809600800609243074268151952075506986744112173705954028708402476984870394191960873402354883924141967550191866699568842907604201375691789447875717904606519911990426542474202257873447575301056799275687300162520647747107269726474224871972583841770132600021392824170364479997596039740110453703092023353509635466916728572484548240377314945387893669640571134322168573530937553272534344026352868086607470719702834083444892288110107409203387500645304783564563250097472203694663921776055854765509655453678476671839476460047809572066383800073578102412448677588127686456580476332728179819886722353651757881477563060082645588924556499391555273484226830089405923826482032061727033691299171028963180078332147031372667760359558227049050997025606644301016548127476540817805335115255877601761512137427173611440130583277311855464175578764391247410560993954608789331880719073041849580910172771756479685430766451208229539612899065008167414362054168182076054905614418672686595489698195205424496943265168367449853961685536803353605862503830213266961051166928463688349071936538327772591282503905358730343548547940428875061176999789174879128250317383032697059739647594262479435287070553934024147200", +// "20686253009137100677580536481772220019834185053674835675104805567705108100012120793382980473459675176967509777404926949610065001902175161695962003614967789069678586166752499518706455413722715678873250863436364658098942388406798677103189295795606470644008251449037179794502035845361840650187691172794383123040477163218916023150334807750836446866590977919532470017535348573419966872388934361829146368421225272657444717715086585757479506344335650280743058062849751597138313964046571213056448713724664410661632559255622391230727347897637119451531510420187884140761879388980580251574156491409434892603700316490405451728370661830847139541500680542314851537865497120978838526245028564455048339534705369938676962653936571813488824888264746297425445127522661324094850525422265769467214262342247384649233154604849204645241734493281272153516726661106475381716751539505876924878328383153759965309983960685978480476384651288642156631803739479287061925347068044278079778523599362177880625669615321469494541417207707795112792614703988712865352607326208856280054996813476487679520479619834119561518751799862129372840860158171276154958342280155058027979175914132253409673754984859885834350221155842157851019202903208837729538330893332631827655821675906374426442436139049029730560", +// "20686253016517652578576226218037280304512998878967782081643030383762252648179296997011341926638803523834137938978407145354361840964277764429298580433833879753046327333287664442011756674514375099551151016760768951640623954608522240638785781825523830834849040469461889966736539399995075932019080426966535734677817177333374424187501199788948653236985818361084089703379091583995023155469499841274582203201561305334645372332402890018547968670807123139871175453983341212876440749594027233646858105401856148507349000386220939016569850622488928617382330345394648656509732599042249901506581310905883901959534984505897371121911753593182717796270161120916944128299041339386928147022131549318082428289605796149115464012497661997457808599992494790605129095762340900378198216634471902525669353909170959057894014738736417606036662468401786737193290238560507341770145447091177773612273526207707414967862580865925752359713241893890750323872352660127060159105979208005350694984418621693785977972962175850201555015110758212910334258690225997862373112062500145196188829278217998167244761059788293227986694796302184050693617864339940026112434075085598165880397454033611127221858601971082649842623914964304450321042940327685124173187185970331014916661615200988009485722532416914981120", +// "20686253011995955629076325009649943801969349915799396834999938713706577453135666219827378331078791305568238767012603523685969479718725348248703275237034007216573140943056580311779171211523596323125369774697691841627573832408640321451759775024971845341190607371902257208390902094844221754727824487661732047560640579666147972731827393052406954025855636608759486135987631984582660021446965995410633211966029298815507401281475655895406689670177716825267171265721675030636564325187593177345954853929509007143136312590872799900706331444637303395615294877562776780224309357978460277807368192971699424316405430599226137562559166681593644350393134128404993203172540565405337112770214130741644001329050834501676799331704650166273747915134031416224513404401863940821611218604578118152404764705070475345051218888969396184751106112136786410948127273335981631264270134095884196287017029694382607779915472082121808328197672918175667411956374486872625493883365083931491511971634291156868483178497893682614629335075497305499292286022408321989969349011048118695523910315223143950295729792988112393289748804875597785632720969020601590483488192942403773245914283442681067607851920760864623779136461182039085569209069946423436337464957827499602113031939666405499335195577103935340800", +// "20686253012905249420753123641394125581338490431246330974155061264735030723090537231131634982701543430151111947340098713400919665484501369074291449678715950324404084971416405468246859755401939519930479946881325066315343108805200689731279192503717023845106229875664749782703938485867918135872531314893451199777947488701264763722901060781624134818284452130770584902522697944415430677834037635796007590708428660357263597678671576847673995935030932886476827700205482688638424266106169588048237006500162050678096674641748475495896688241776715995439349517453160862577278522303148756730047750797909055062592477399585648804967165274624689461786686029846025735530763855449050156214748263389735761523970386426501083016336782069534800132132814441821194458883018197602204145921510758387553720734875367542620886529149295030141428678136073816645318546217907182935719212846985527885268490931042846941843895577575986868990275246964000930910996610476509616118972208655613370459666905798364207165929375677455372211850892032163428761255574831943581170173798962498310302619079810560252634395602186653677496588976269539691991643655645015285991996519815871201426765673211289585535433601874045698363351158020555486412212492888981581014428858434326634365544072244470887968780251830419712", +// "20686253012482713944170510486006503495965426418624979215207888186336584311563125668176618892341461495958989530109497058836208707044168245279695940788984032479278397649608713044920405685548592937000089068732569152566103178304391968262275366634982743672579525342222333315935193825742608586653438687027252465742150192128141744663537027469859051411958033527779975309458836521627355592244019941914250716050720601976679339560457780808022948385738031590113665471354096086486875697653599495164469297979201195396357751415927806268197841979271997133417306549038027016271542831330284708665839765584145061136794149016409901583479427347126229617073748763355410944442711689578134558428523469322867841307165512505150561440558923516413487943512575228004609949233825559134736566257971598139983362643764003946061810478982678081113673133692828469537444314163454166205388851192666274080898281014667651235171324569407150427688362370268502748192144621240298284168580601438138025554803091157078261255439130748192811252524386962992249383432688422311292315445533782932932345446806388697895661377268890467142415746788623768632474596533761037877631922147784592888033960567149861018268485072235834034618474495971824025999049529331906554158125086921199432771280915863695896037886622831739136", +// "20686253017596800673636777315187534270021266420333274774530680374903816600749071306262945801692120725976064051984138994058746162072937121793853747964627815552811045393684927849560459595356986505364881801969476935753890194193700810599429133930398705250755254836009280177917842707354484199908531777573893797187673907712955690758349411755815598103902935655017090298516565740392457105436565547185001255070561133895788176000782782281412093088710504721088727045238362229707513145743387163755472103144359822049806563915934557475036319622688517527471037575232699010534869068383647539493972360025529780544812641848222048512592585298352948703198597172454291713942776468719578698387846171669051363353131159533380505301214164797646016372174811126560901763198906646299690775898549445445030438862050154367175654798908548309949046431701706518418835907611144671153892500148829267345643289528684512615681494813120785604119244868024564867462454807660897301092541915645564379781995052629376347977326432306238090292830317108743463398855319724461757906202501054760315093634564844367373695116166888912389471388175972952318344253589126281155102370809690828946212895190035082791906579557462335096539646297577735157137081490920968937162188359746613039900117481527765337456961996600639744", +// "20686253021670819484641397455235988893189004357659986764689257803151409177614461364649970433006714126825319428176308950177181495096102292376641455488005607469636772210940591752734984815566464618433768320892209283008301650256134897180639126904431683668620836360770522622987413924299753298563911962706925772038987129402614215060442406183258855362002322270760435388491304782642695492897104954057313819686015892539037035024067606433686326406317580126032026561653107203711784714174553258867988922391584823277517348441610017887616725654033103467290522569906901516453939475371008093949281078143911461551090436073001946185740446013553585657161583955882953841177944465026376300855580949947151004864741065528954219420584830087663819106583270144309975996802314686009200802567965571029611087066652726123514794266804514760076981917095632831537957124002094177446121777714425501240501887464206036422959211832442482836841510124546833303416051006026030814775613976153585582885196096288806254708297796542938960077290086802302255719602193211623734787514863004972159363553648873516823809184020531994302661726715338441496144201492168521781315979852201195520642908900413827363866480500192561642524090584879553530477247358859671792789145550860764422409857398848511879706827894470738176", +// "20686253007779418538146435611960103480111665071547886109640735401007273574064776615540701179896428377142052442567006099697162043246765708980976145988576633992791951978052644497415923662944617922884643981724695008383893280530637173581248946990739081903596261263913018197718895124329861431222284653233340024925156526473018621659579602661924631346612848404894851502528305401866261493226279256928046285934986381048052034850172444367408714525087992488005859355731486073285673373650699722735519641713322832229543168311432490222848569998028342925773025050691460124263527225809542829858215777298289860007795297848906118975152353977610454216097635614989263760656961019437297514661609993804271689676470502860376666591936653476564447284287948444996394814992534945211106658510902269472032893555816145339327183535542766040947125922850444024392962311389749647694036921114853739217072675694979896955780825486065012334086560739968331771161859717998929049806691359069902247801751447279973168812586166245092210343145733018596567430818598988422806423359671179738425327829924055793651209866185908863272645671672053161275134558190614726280574493595200071091284480123481958235975875823917688354274132904898857900963754501168982796149653431776796588769427548375743948019813104125739264", +// "20686253007604876142864868042280524165402934179882610914697720762337073476216917389996124773136098970057364791099676488858171958566606397819353213332750920419342798801931043028447913975575839783079948872757605203684602892225855510703027538051098416020818203339920511893350539925823111193247570595010592164929661686244708062422421354240082899804498188803419141823510080225562148441961167143787065210061597742895645200944179873692681009532549500390826790962525777523907581122863044028963918723919775810059776782603566022001398041618219382359180430108215466147420894596600846280899658544993640061836655948799058416531930107797645597300743718230009291894571536654250713613225292199432580280761478601571747214473410393967212703089811962847476443829426705969655282817199293842112005075777464459419904700085996691611909093195963267420749267548377551222829489604133422219862110546274841495019087571956520965526972054781444561101065107965888687457231748284726446965643182772550915093919251096627310403665749159263278352617835127791887411059400056120654704697281677759462412423949563123799300922337337031434011248992699167653294572541741943792138821657961461481276538506021735660790599721725446180641231442745067805783335704101764512016456627263309212202141050291403882752", +// "20686253007021608241689663659963106759118723298198840826493322751986448442693666027764988836450224647331743582291670090917210382327270531482163927227988223092974790331550687752773643667187876554181907548400902488924327949585152208488928230350565913895512207957249179245514039152326127203620938403901730645311641228704710362766137452100607502345181504058992226565546374405238584328644321921093037858527565125084661975400406032983072371091348426803460064043823708388520703550652014817583643133020229845020242944346824617584906011027684828450454972028242671768211013258545366598182329727396593002246615440504964904310838157610000834126195478113929502258505249404659889702740225008795785252745301900749237920017295319107372211580703483426556992675462580169514689033492702354908848878614127325367288349925054680142050382109615731173807465659057525671650993994596156034190434249834629081700288210478883667431390577000482807339023985920513954187158118910393481098591489866417654826052352494975210865965568210016302565250763960252121003586393653990260068738875254130452064019745948999144292814505789232823453267156783611177723108336077508126670001743428554516943943698961609321446420452826416665860184743437533100494614025619487465731926709595422582461864066896099279104", +// "20686253017576328604397757628696916976575522581262455658158084545267679494101813744271361066833895727275999990362314163487589781714736086134535643149499659121815284171966664818069832388563106430729424843320170501399681992962816815405466327181883993596336326588913980995040153015039842498603793624311027080239766082382138185011127721145696561736552179519972972189860686192212873871541534333569298609110022068676249161087325016973188219381193106238748350352370017704856280849939084182123474167745313719296726857149643911426423892259144920430332447233445727394410596908871121943691683251347594543016832133353836309946329620336278195398229250583694331105065483574747460477844028959296763798522682650225035915279057821815082477316716472233093622430236186011224736996806782238285457943841178908716320629716681607442109914452439698030106201073955996654282803838012221802786122722463668921800632723654622027772616422218197375832565654227089919843040086396119306096757556190008760084017430688032081654953292207865443621116020116320243741976393955557450724420382106551748948637361361237424102598662130622446951212397620681130020351934387640468968673784832844072192051121879959906157803839174272822858710610565971349380572855080734703266039110827437002755727257645879394560", +// "20686253015632194446964549752865306095675796992984280836459827244594988985728341929467828908174136317388413601759640625700697567744571888535192920357933899911995149737015137803918541787810386447480938150007979984323709223630514858926018913313903533635089468154621298339064447275675686499058206570127524798025633713627136893628055375633970109073821185460898039856680258335615963791682491988980277498832573429849348764105832237085843498643498195695163785310161310344120423648634988353729865581801558283805742288660458122510719188397548085580202887930607910533534516693933657267260780859925051022338611693920561790642016487656354781128170103744324893680564297782645461578140938361264816397758641997289612268599215669012143301913510494792757326575404821915282598318350394530566425173158364070432671579795603645149354325381596532039075427958043639242360621114659751231872073036120732181043105972973109788769983560711082730917065413254697951492111991932795350395291042777547404358191506284743875086602012076691203993957228415814341341317076567346863011063363884150995846166969589047317632929392743149362575140604429955129918452363340274074095953595228692113309367060073180152866610193934981631436082786314283160713802768267996895973676591655886961137949790395651064064", +// "20686253008280076434063627792976585679258240901117771774848183680897991358808598372294247945349776781930725891159660642772324594460473591428092423907922733507827637130679002417108815594381100952950780741331843640801790721759935458380251498677194095462459535511544103447989319232932465020496572704531324890159727609395394440487675667733293698176844674240132373049476798001543842889891227651621801389443373039936389118507727414268889974019400734622787942381724850321036295849698333470872363731655908573604683885345765197680079122655638597424255830729241057473120238971524652052886581049260033714044170704671920548231649930751117900234527322744641536960791319040850129139290431448454658633022000433479483039128614980515014378005152986215764207972187867609137628051392176711451953239313092106278507175222152806081191880959633861865125667318317791167731714976299042173744637249086661016974783212613211649879521639713884566600223256092655930849081112415596892313717463722708665626233145587935660772625224236074541613317671527221358656944545879413948646453563296121659821042385880073296612404056486812912561838422746761742255638925665021058593318024264673057295116442867443437729038595456425369615349800800527803917805871133175616209136030135179809038417225106990301440", +// "20686253024951658247963784256461033211161677366121981865435929241662537577939141826066423582382620272467025896961525533649529834951928815732696192031254242001310976196395313722735262686979806326419012885189967369774411212706004362512392149609842235706677304433133443952583032520610040362070455319152450494660815670160633796512952042902222316108338953442388922875611033294149441219453308994445197831263244239682760264737410367007220572112879442727200430192143800723337291968248880476611034380579121992553429999072019510264034072224367169076681647830266796198698184376443826044285160611787998026826563875013778342721603031807058331671932037301518976354062079082150728323092697138014541325857426045339923252246197903880065058351701199295574478678901159403090260453086693563410504667697340773698567005624143430980337698805702319491020156284314353992648718466728823214596123324888480179957675142649621354384527020551369805874882218433548180164260225089194029633825464003674135793863996433213944915953458051073341886199898474985986664620533716615542750212008869522238677744797202727258018045172140382145668069884754503189079041670422324774317816739968675502203964561699314302235308312748370659797007514871638586017968916729509284382607208921962626655481606857704866048", +// "20686253015329051263633614301062841926780457793244333362445652976039258394085904409997569212036513569627020122416105184089156035648212037623378445311136404144169674742143343217136229368645245696403751711017904206141255246181738620208604563131352355142398183885029711993169895631312556797474335190203415666424731815815318567886625912492670817003495147968334343459502296653971351530810954197377406607923523701655523747303747572405902504136614192793569878303382172717914572317866962252666818690300625318696007782616025654101394182820792992039050797265658839910245551706198215350312385971886655288470117569599164586651540666084220530837687983855363703457438523945771377188384096611403701921732343378131772105605616111230060395767250296130321274230574922212950953525510434760474902956273375775869424140206127230345610638214846167903417131896656272090379493970136500706006844054279981738876197283239177788834113814444218385342970502089028750415245685729607184037765166339498069290010361131234888359345071611741250271620624165209355771772144843622884733754033577735187746521804131583913214093166309888724137377435858133185966519694264750031605141214041348529307409532348278522558737337691108243193690034235284716283236278857093221201219489501714217369972614111881593088", +// }; +// private static final String[] SIGNATURES = new String[] { +// "8528503594670204436052357015928862792506358113369106777406225289021120755325204284915095461436534593726527514965930392069276752206673637292335104066658988404928981760593418142871524780306395774215697719603009673950459832507052700686367128506073190142373812763000304886902929199810111204681208295515597746727406521222365667866848729045254159801613868059972975579907523041955922782192361370263034597048555422592874100353115430271270939317456318510451811646267157137577724074073241955677595103004166831493027134464898153615411682508571271524917051485420113620183698439910489332188676948813054298376589190870205376721722", +// "12714752653171379071902223518900086455469254778099176783756546057501252740388760646633111438554988457804110827172122185741643374376377476845818226306550070549540100924375173033053872725592206270543865057909506204686416217217175205975936864769675359443835464932866652953564668335772839380415278907842078521565813907913565738769303530371937587632865012931316702492771417680964122472313546679654313803229834342261250191743408149738051095079442654981806457495648490488615580878652623030639431127849097089635678230782461813874847730298433045763073635235796063186457060470168072867337516295219377274643254214166187741928195", +// "13908999250565370485651189816621604309406838390888107327583648160922258563174890896666525571590573441122409145906761904814530942061844184451657972503856070661891327123507140696829535452282425270195643203742925435202847440053652045520143275171734902378485911971935113470515174453988792439225620093679659059409128896537294846575246782278559049832955440237015381818190544157681329515828756452798679892374749946754033759443639475313334060671371142984239868008118691626803951984815668325843801784709432199331447858105577423245817671538352849819889855177911611607915665983585833151320422174088007859931381348016319265461694", +// "10663928803538142495874798753524371044109713444112476166920999738661408452717517594386747916849812868865845184227592286809206405151706316864404978033443369238163211298053129657897912511475878426734168668138448142684774180483848489762892194047712675443661602073836529221861198253894032646440612405989149292046072809162239075060183622012102135247005904791258725387249405763973030121585327387996523577075440922630211242753391888083653575027601995970547566022378966563679332453413606097684906558136475009659092263370997631008298982911150670427099551097448345541506637646597809408731612314781030427962298783217248914223806", +// "2306715670154317872337078201496706067018016875073166641656805679181208518844313408613041429049277142712575268846333720788389889629069536193483907615434194600534988985015385608627085443890672124721486043133411014103460576453162713180941943637321699804181949641374502608547423908202551134802437475178385172142729079080049166263881079442027408091314096230581266354997604485294706409288693050419576182967429750806152579311940537301588263217818952406077159490924792302551188620989397020490518323111334752644091498632583425115828895844183618266417719447627326662269293970039782216019092059851746977085656284304341508057993", +// "4659358238164596158948789743224240069711265838365302036573085292613329291682190238495190399519475812546652724862729777463345410534406299261146698284440785385833664987236353362536409224868061055771434659441519724477807214986173373107393432602854768393738777892796122503107069552589289874263521713955968508746406485296523435090470754765084421945615052354670619502660960366860227870952975168583254800217898305238191480303688181715819411899720342242163096384819366364486118817498794842558871973328704472113837172120537734616429530518033340109825721160272151409573547421252939751876457633571421692240302001467345028442601", +// "4049477946498817875644423736582614129429104652925293903578491350187944109357140281079215461385310310227699148230226103813511093858314731577806298771933039828435988536582683795573155489962134767330360695808904080698959547160137535666545619466761042007784679323603927971926202759931895569401425552243257170909460081819203771507281795697451004296311404365704082156689073796657538886169509765942854790434677625126588993604623270447590077611239790400219558927531394967281599304514894414627660359455930701315462448554588404533268609540765516347343066315049932609011242206592126920115645749055638930724732121135122116141166", +// "-15821074705595424775489985353461364659530670829708431022534131332700730014024858879661015439321392526031983716218847190089100850526695320564737549648506802838787532008475561270345163817098663400387502475397954072183343244145179711236049401253605959260401466676172293703977395461139502392520912345905534309764600115973777897708515531741002585035454835142666002006897210376288419070871163424306698312361191916347081600355010527694492360779722717717750770230108246572032092508371533877677011198486557314243866055301211143049322569030292978261520962920278653892612567676627225563117772597001334618978059073836056716669225", +// "-3253944899977321876942783783337240903554123129096642235321635334925673112914508303442241100990114967311556077951307727781710343006400897352395067739623245112414217140757020483043902863652615234151122932808390646136142553799710067271445726854624819815642073592109754781288339958163810012173119634614599349819358363736874502076277367766337257137053470157924537552941473944009380652976471624978609651165801832875705879218057300556705292310854083054205343202526672473020368237867322973861758935442876895613369159646304355164238984667221571500917386264593984463607981687284420899772355044207890979293151892151717827509819", +// "9499141240577043451589632252552399646757731315654129758760015349045571672430739028949896539759619921365912271793699639488523910752170974284878504952562109143512853537668435378131893436415070630973505762192641967189753052893201272349296728232365714025670987726850356205941217266924338273790152610590219162996666427392130736201968771225650743588893284231594006554226911151399044815787836234434162548055950690340366254107040568369155820242765188526434944719636880243193487042089094702078107054425645211148742327810444676596010608038011230808463633129576609253919361989917628880939682855954355300546341301909315104641292", +// "2785829464159962785792464154214301170737801888846376572507071703868621757492888529934734721293299181626548638776917276845515937348930186241646686214395195802058604838439131193816292088810714374344194956806787815938869239553710725569223765884297868657637234129941827347968009169909865231403471928705479925843478070964175560548021424092972184851431376822977172730546215170545593072337665216944481311725273881895390870402865177526671062318915225337990250246626982564561668482752340309344623795724163702528068799345034816267947632414651344549931495044609876171529597903856776762161922406306755255424075670945233876468235", +// "9693399573277126168117955145498856804911026370939734084616110928720966781810687706982467213763886301391323622148228097849029578280382356919106089289850794524605638208088414420886934380985144626389853251740087295754111363849778298354982244500232225340060817412736334732343104887783650252691586118114592726131559322014895783483438914376642606356658629244848851203222766693264106049037206515886724418160132270092812177670695280590828717742154134306911500638045109639882998686085462128237114003344000744142984004964727666767343836058943694299268149858387245532882552313792450703351859102940672866025492125862734810702868", +// "2588026480762077299880435211630462130829042494172803139844752929165151314366576349692479708707902818325002447901174618910914735813021836209789642281992964413385413651230009333804422618970344994918374367306597128918353293939655079898804834272495466247472976936750289217893184038643984663442084787174472727556425006629985118671735361758704886890235923409705820370265894413685399080693702003337025366946813158285546498272869112876242928634530360165484359119404508056677189703714418493887546407516490045931543871651300601164453872612008839158278878642833640818155397129455955015836831802614094386547616288007668519396656", +// "-11728748857916220938525563370642851174888323460551443012033711709118634455694860641893644284241538905301760022334170567702665954472646365780973999849934512640749241332019089471035867766088117990425231025030842560049003944730933840471815845647939175446669519176892871100667327144122288934729242175451319185176733723593770503584081246166626636645030357166311144861050296775574567995193508629237812544699293985433002148216129743645932049717574867996068064127541167278296952065930035012382421475825376778590937794554225647092055932998039404832926821724386093261449972691735490787917635072888523885575813644811005358833688", +// "-15229937207665399077858745779056928142076482322535266981438711930746572914030457988461614013826308364893535928521195966837848031188929394769614489759198148009387852082467397161392193074580370561838070595801238055150547001898037272379177792186097373425590642040064504319592980875488946638406365056236476661594999049247103250079448049760186508271091630332833596279458697068078832218087075544964591498372812863039399793891647438843170406180128852474289724184601066049404074247449524559845321683147746932532594721096776453826585995068663187778354136614612407086729935627102933428001169544176494702260120088616177094122596", +// "4423817485097350932424418489755694412699593088843423207118652642017753124828670168023683986663483348010821563740973349339039999652593572489226634061594354003970858509410018682617414793335864951696251946617385611984536476771260728219342233573060916777310319872368264904534560004743365113463062419519791519083814132572592001715626074081892878836026456243524878087931315541387440248693791684485700635565529131429893772243402800191002799200914843481323690852313588145236316661063827783308810378121509659713394573216583671878018935266378492137948180346291679018739284608191825337179228499660670255782455554732811032893595", +// "14060727610797171065873730743344628826300688857981195853338897134682600312555998942680998373000530961057721631672534399458386502204866782136713687245588626842312961478612189645195715241803501047584717823837224418642078891142575255896720379626691373477223808040839453370799715426143237974402342331020941533125564932963228074818891707580296516262584451816695873169230235668081091425131622898726217356123668519420820803676318446901933688924722455581079574809688082011559628335681866023133655136379898615214209349346203435853293689318189116037465645474669519318589567809649024218325830547412938703120625760137723658167545", +// "12143975642996547734993210479666904095397741261764866078090510262645460132290235986630376713004350672851651807054875332939956731775733959997351324143497076406076776154977723396094902338839439098161231984686885314849947129332117197645344151755344324038625202859604817402251985217489847067013287339738384296915967594310421751908244841803407747710371415190598155158739898568686373111717605879278384135851318969079281050507414909011506576169586220120054116056221990949404820925350166961128163192137909212020962545745326872002538950594091355414304183527270036601878504771221000529804769425263948649859429914831451793830447", +// "7681905128991420738358979923777240433337546546660192093231163456029821861424188702648102462301544733499592591906609649350535414419477664467070025593525931603028515171360109575816154776046183332815807349628739221532165348992025144025203444717113795284728750784856862721905401143841702874481392901879089726940312479501647028937736816673686877284445430463537947530572199915087117895344271025474693120828230653492777168767540782277375513209223619007329449471451811372004116502977451480549159579358749540564515729677785198516083957467543681269622455949740687260144765977931043642705413831493101417185777391942989245676973", +// "6324957736802947762698105017681228051594271495029008384509236777170014306498658302206164665097448703570931287007659305566350360036085316522653055137878512074480755437541173568670880476898182300821763522340043814781148109583375526242284492181946863678861291620814183269643728258345959383134128289995080908583694306703558113066837628145217885578776624299254179033312744171665733436458179725336656848290970572987456090589484353899143202322971998318634185334126489034866595781074592194167476225131764951162637607536439161513547138214421713349130949142742184752353531752780426054707814473818221353155710305120310324535073", +// }; +// +// private final RSAPublicKey[] mPublicKeys = new RSAPublicKey[20]; +// +// @Before +// public void setUp() throws NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException { +// for (int i = 0; i < 20; ++i) { +// mPublicKeys[i] = AndroidPubkey.decode(new BigInteger(KEYS[i]).toByteArray()); +// } +// } +// +// @Test +// public void decodeTest() throws NoSuchAlgorithmException, InvalidKeyException, SignatureException { +// for (int i = 0; i < 20; ++i) { +// Signature signature = Signature.getInstance("SHA256withRSA"); +// signature.initVerify(mPublicKeys[i]); +// signature.update(DIGEST); +// assertTrue(signature.verify(new BigInteger(SIGNATURES[i]).toByteArray())); +// } +// } +// +// @Test +// public void encodeTest() throws InvalidKeyException { +// for (int i = 0; i < 20; ++i) { +// byte[] pubKey = AndroidPubkey.encode(mPublicKeys[i]); +// assertEquals(ANDROID_PUBKEY_ENCODED_SIZE, pubKey.length); +// assertArrayEquals(new BigInteger(KEYS[i]).toByteArray(), pubKey); +// } +// } +// +// @Test +// public void encodeWithNameTest() throws InvalidKeyException { +// String name = "MyAwesomeApp"; +// byte[] nameEncodedBytes = AndroidPubkey.getUserInfo(name); +// int pkeyB64Size = 4 * (int) Math.ceil(ANDROID_PUBKEY_ENCODED_SIZE / 3.0); +// int expectedLength = pkeyB64Size + nameEncodedBytes.length; +// for (int i = 0; i < 20; ++i) { +// byte[] expectedEncodedBytes = new byte[expectedLength]; +// System.arraycopy(Base64.encode(new BigInteger(KEYS[i]).toByteArray()), 0, expectedEncodedBytes, 0, pkeyB64Size); +// System.arraycopy(nameEncodedBytes, 0, expectedEncodedBytes, pkeyB64Size, nameEncodedBytes.length); +// byte[] actualEncodedBytes = AndroidPubkey.encodeWithName(mPublicKeys[i], name); +// assertEquals(expectedLength, actualEncodedBytes.length); +// assertArrayEquals(expectedEncodedBytes, actualEncodedBytes); +// } +// } +// +//} diff --git a/app/src/main/java/com/xypower/wpywapp/page/BuildConfig.java b/app/src/main/java/com/xypower/wpywapp/page/BuildConfig.java new file mode 100644 index 0000000..6f61cef --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/page/BuildConfig.java @@ -0,0 +1,10 @@ +package com.xypower.wpywapp.page; + +public final class BuildConfig { + public static final boolean DEBUG = false; + public static final String LIBRARY_PACKAGE_NAME = "com.kunminx.strictdatabinding"; + public static final String BUILD_TYPE = "release"; + + public BuildConfig() { + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/page/DataBindingActivity.java b/app/src/main/java/com/xypower/wpywapp/page/DataBindingActivity.java new file mode 100644 index 0000000..ae00dd0 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/page/DataBindingActivity.java @@ -0,0 +1,70 @@ +package com.xypower.wpywapp.page; + +import android.os.Bundle; +import android.util.SparseArray; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; + + + +/** + * Create by KunMinX at 19/8/1 + */ +public abstract class DataBindingActivity extends AppCompatActivity { + + private ViewDataBinding mBinding; + private TextView mTvStrictModeTip; + + protected abstract void initViewModel(); + + protected abstract DataBindingConfig getDataBindingConfig(); + + /** + * TODO tip: 警惕使用。非必要情况下,尽可能不在子类中拿到 binding 实例乃至获取 view 实例。使用即埋下隐患。 + * 目前方案是于 debug 模式,对获取实例情况给予提示。 + *

+ * 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + * + * @return binding + */ + protected ViewDataBinding getBinding() { + return mBinding; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initViewModel(); + DataBindingConfig dataBindingConfig = getDataBindingConfig(); + + //TODO tip: DataBinding 严格模式: + // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, + // 通过这方式,彻底解决 视图调用一致性问题, + // 如此,视图调用安全性将与基于函数式编程思想 Jetpack Compose 持平。 + + // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + + ViewDataBinding binding = DataBindingUtil.setContentView(this, dataBindingConfig.getLayout()); + binding.setLifecycleOwner(this); + binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel()); + SparseArray bindingParams = dataBindingConfig.getBindingParams(); + for (int i = 0, length = bindingParams.size(); i < length; i++) { + binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i)); + } + mBinding = binding; + } + + + @Override + protected void onDestroy() { + super.onDestroy(); + mBinding.unbind(); + mBinding = null; + } +} + diff --git a/app/src/main/java/com/xypower/wpywapp/page/DataBindingConfig.java b/app/src/main/java/com/xypower/wpywapp/page/DataBindingConfig.java new file mode 100644 index 0000000..37b6af6 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/page/DataBindingConfig.java @@ -0,0 +1,49 @@ +package com.xypower.wpywapp.page; + +import android.util.SparseArray; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; + +public class DataBindingConfig { + + private final int layout; + + private final int vmVariableId; + + private final ViewModel stateViewModel; + + private final SparseArray bindingParams = new SparseArray<>(); + + public DataBindingConfig(@NonNull Integer layout, + @NonNull Integer vmVariableId, + @NonNull ViewModel stateViewModel) { + this.layout = layout; + this.vmVariableId = vmVariableId; + this.stateViewModel = stateViewModel; + } + + public int getLayout() { + return layout; + } + + public int getVmVariableId() { + return vmVariableId; + } + + public ViewModel getStateViewModel() { + return stateViewModel; + } + + public SparseArray getBindingParams() { + return bindingParams; + } + + public DataBindingConfig addBindingParam(@NonNull Integer variableId, + @NonNull Object object) { + if (bindingParams.get(variableId) == null) { + bindingParams.put(variableId, object); + } + return this; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/page/DataBindingFragment.java b/app/src/main/java/com/xypower/wpywapp/page/DataBindingFragment.java new file mode 100644 index 0000000..db7b49b --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/page/DataBindingFragment.java @@ -0,0 +1,91 @@ +package com.xypower.wpywapp.page; + +import android.content.Context; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.databinding.DataBindingUtil; +import androidx.databinding.ViewDataBinding; +import androidx.fragment.app.Fragment; + +/** + * Create by KunMinX at 19/7/11 + */ +public abstract class DataBindingFragment extends Fragment { + + protected AppCompatActivity mActivity; + private ViewDataBinding mBinding; + private TextView mTvStrictModeTip; + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + mActivity = (AppCompatActivity) context; + } + + protected abstract void initViewModel(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + initViewModel(); + } + + protected abstract DataBindingConfig getDataBindingConfig(); + + /** + * TODO tip: 警惕使用。非必要情况下,尽可能不在子类中拿到 binding 实例乃至获取 view 实例。使用即埋下隐患。 + * 目前方案是于 debug 模式,对获取实例情况给予提示。 + *

+ * 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + * + * @return binding + */ + protected ViewDataBinding getBinding() { + return mBinding; + } + + @Nullable + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState + ) { + + DataBindingConfig dataBindingConfig = getDataBindingConfig(); + + //TODO tip: DataBinding 严格模式: + // 将 DataBinding 实例限制于 base 页面中,默认不向子类暴露, + // 通过这方式,彻底解决 视图调用一致性问题, + // 如此,视图调用安全性将与基于函数式编程思想 Jetpack Compose 持平。 + + // 如这么说无体会,详见 https://xiaozhuanlan.com/topic/9816742350 和 https://xiaozhuanlan.com/topic/2356748910 + + ViewDataBinding binding = DataBindingUtil.inflate(inflater, dataBindingConfig.getLayout(), container, false); + binding.setLifecycleOwner(getViewLifecycleOwner()); + binding.setVariable(dataBindingConfig.getVmVariableId(), dataBindingConfig.getStateViewModel()); + SparseArray bindingParams = dataBindingConfig.getBindingParams(); + for (int i = 0, length = bindingParams.size(); i < length; i++) { + binding.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i)); + } + mBinding = binding; + return binding.getRoot(); + } + + + @Override + public void onDestroyView() { + super.onDestroyView(); + mBinding.unbind(); + mBinding = null; + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/tool/CommandResult.java b/app/src/main/java/com/xypower/wpywapp/tool/CommandResult.java new file mode 100644 index 0000000..af1ab53 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/CommandResult.java @@ -0,0 +1,47 @@ +package com.xypower.wpywapp.tool; + + + +/** + * result of command + * + + + * + {@link CommandResult#result} means result of command, 0 means normal, else means error, same to excute in + * linux shell + + * + {@link CommandResult#successMsg} means success message of command result + + * + {@link CommandResult#errorMsg} means error message of command result + + * + + + * + * @author Trinea 2013-5-16 + */ +public class CommandResult { + + + /** result of command **/ + public int result; + /** success message of command result **/ + public String successMsg; + /** error message of command result **/ + public String errorMsg; + + + public CommandResult(int result) { + this.result = result; + } + + + public CommandResult(int result, String successMsg, String errorMsg) { + this.result = result; + this.successMsg = successMsg; + this.errorMsg = errorMsg; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/tool/LocationUtil.java b/app/src/main/java/com/xypower/wpywapp/tool/LocationUtil.java new file mode 100644 index 0000000..699f68b --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/LocationUtil.java @@ -0,0 +1,282 @@ +package com.xypower.wpywapp.tool; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.location.Address; +import android.location.Criteria; +import android.location.Geocoder; +import android.location.Location; +import android.location.LocationListener; +import android.location.LocationManager; +import android.location.LocationProvider; +import android.os.Bundle; +import android.provider.Settings; +import android.util.Log; +import android.widget.Toast; + +import androidx.core.app.ActivityCompat; + +import java.io.IOException; +import java.util.List; +import java.util.Locale; + +public class LocationUtil { + private static OnLocationChangeListener mListener; + + private static MyLocationListener myLocationListener; + + private static LocationManager mLocationManager; + + private LocationUtil() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 判断Gps是否可用 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isGpsEnabled(Context context) { + LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + return lm.isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + /** + * 判断定位是否可用 + * + * @return {@code true}: 是
{@code false}: 否 + */ + public static boolean isLocationEnabled(Context context) { + LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + return lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) || lm.isProviderEnabled(LocationManager.GPS_PROVIDER); + } + + /** + * 打开Gps设置界面 + */ + public static void openGpsSettings(Context context) { + Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + + /** + * 注册 + *

使用完记得调用{@link #unregister()}

+ *

需添加权限 {@code }

+ *

需添加权限 {@code }

+ *

需添加权限 {@code }

+ *

如果{@code minDistance}为0,则通过{@code minTime}来定时更新;

+ *

{@code minDistance}不为0,则以{@code minDistance}为准;

+ *

两者都为0,则随时刷新。

+ * + * @param minTime 位置信息更新周期(单位:毫秒) + * @param minDistance 位置变化最小距离:当位置距离变化超过此值时,将更新位置信息(单位:米) + * @param listener 位置刷新的回调接口 + * @return {@code true}: 初始化成功
{@code false}: 初始化失败 + */ + public static boolean register(Context context, long minTime, long minDistance, OnLocationChangeListener listener) { + if (listener == null) return false; + mLocationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + mListener = listener; + if (!isLocationEnabled(context)) { + Toast.makeText(context, "无法定位,请打开定位服务", Toast.LENGTH_SHORT).show(); + return false; + } +// String provider = mLocationManager.getBestProvider(getCriteria(), true); + String provider = LocationManager.GPS_PROVIDER; + if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // TODO: Consider calling + // ActivityCompat#requestPermissions + // here to request the missing permissions, and then overriding + // public void onRequestPermissionsResult(int requestCode, String[] permissions, + // int[] grantResults) + // to handle the case where the user grants the permission. See the documentation + // for ActivityCompat#requestPermissions for more details. + return false; + } + Location location = mLocationManager.getLastKnownLocation(provider); + + if (location != null) listener.getLastKnownLocation(location); + if (myLocationListener == null) myLocationListener = new MyLocationListener(); + mLocationManager.requestLocationUpdates(provider, minTime, minDistance, myLocationListener); + return true; + } + + + /** + * 注销 + */ + public static void unregister() { + if (mLocationManager != null) { + if (myLocationListener != null) { + mLocationManager.removeUpdates(myLocationListener); + myLocationListener = null; + } + mLocationManager = null; + } + } + + /** + * 设置定位参数 + * + * @return {@link Criteria} + */ + private static Criteria getCriteria() { + Criteria criteria = new Criteria(); + //设置定位精确度 Criteria.ACCURACY_COARSE比较粗略,Criteria.ACCURACY_FINE则比较精细 + criteria.setAccuracy(Criteria.ACCURACY_FINE); + //设置是否要求速度 + criteria.setSpeedRequired(false); + // 设置是否允许运营商收费 + criteria.setCostAllowed(false); + //设置是否需要方位信息 + criteria.setBearingRequired(false); + //设置是否需要海拔信息 + criteria.setAltitudeRequired(false); + // 设置对电源的需求 + criteria.setPowerRequirement(Criteria.POWER_LOW); + return criteria; + } + + /** + * 根据经纬度获取地理位置 + * + * @param context 上下文 + * @param latitude 纬度 + * @param longitude 经度 + * @return {@link Address} + */ + public static Address getAddress(Context context, double latitude, double longitude) { + Geocoder geocoder = new Geocoder(context, Locale.getDefault()); + try { + List
addresses = geocoder.getFromLocation(latitude, longitude, 1); + if (addresses.size() > 0) return addresses.get(0); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + /** + * 根据经纬度获取所在国家 + * + * @param context 上下文 + * @param latitude 纬度 + * @param longitude 经度 + * @return 所在国家 + */ + public static String getCountryName(Context context, double latitude, double longitude) { + Address address = getAddress(context, latitude, longitude); + return address == null ? "unknown" : address.getCountryName(); + } + + /** + * 根据经纬度获取所在地 + * + * @param context 上下文 + * @param latitude 纬度 + * @param longitude 经度 + * @return 所在地 + */ + public static String getLocality(Context context, double latitude, double longitude) { + Address address = getAddress(context, latitude, longitude); + return address == null ? "unknown" : address.getLocality(); + } + + /** + * 根据经纬度获取所在街道 + * + * @param context 上下文 + * @param latitude 纬度 + * @param longitude 经度 + * @return 所在街道 + */ + public static String getStreet(Context context, double latitude, double longitude) { + Address address = getAddress(context, latitude, longitude); + return address == null ? "unknown" : address.getAddressLine(0); + } + + private static class MyLocationListener implements LocationListener { + /** + * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发 + * + * @param location 坐标 + */ + @Override + public void onLocationChanged(Location location) { + if (mListener != null) { + mListener.onLocationChanged(location); + } + } + + /** + * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数 + * + * @param provider 提供者 + * @param status 状态 + * @param extras provider可选包 + */ + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + if (mListener != null) { + mListener.onStatusChanged(provider, status, extras); + } + switch (status) { + case LocationProvider.AVAILABLE: + Log.e("onStatusChanged", "当前GPS状态为可见状态"); + break; + case LocationProvider.OUT_OF_SERVICE: + Log.e("onStatusChanged", "当前GPS状态为服务区外状态"); + break; + case LocationProvider.TEMPORARILY_UNAVAILABLE: + Log.e("onStatusChanged", "当前GPS状态为暂停服务状态"); + break; + } + } + + /** + * provider被enable时触发此函数,比如GPS被打开 + */ + @Override + public void onProviderEnabled(String provider) { + System.out.println("dsaf"); + } + + /** + * provider被disable时触发此函数,比如GPS被关闭 + */ + @Override + public void onProviderDisabled(String provider) { + System.out.println("dsaf"); + } + } + + public interface OnLocationChangeListener { + + /** + * 获取最后一次保留的坐标 + * + * @param location 坐标 + */ + void getLastKnownLocation(Location location); + + /** + * 当坐标改变时触发此函数,如果Provider传进相同的坐标,它就不会被触发 + * + * @param location 坐标 + */ + void onLocationChanged(Location location); + + /** + * provider的在可用、暂时不可用和无服务三个状态直接切换时触发此函数 + * + * @param provider 提供者 + * @param status 状态 + * @param extras provider可选包 + */ + void onStatusChanged(String provider, int status, Bundle extras);//位置状态发生改变 + } +} diff --git a/app/src/main/java/com/xypower/wpywapp/tool/RegexUtil.java b/app/src/main/java/com/xypower/wpywapp/tool/RegexUtil.java new file mode 100644 index 0000000..214fe80 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/RegexUtil.java @@ -0,0 +1,152 @@ +package com.xypower.wpywapp.tool; + +import java.util.regex.Pattern; + +public final class RegexUtil { + + /** + * 验证Email + * + * @param email email地址,格式:zhangsan@sina.com,zhangsan@xxx.com.cn,xxx代表邮件服务商 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkEmail(String email) { + String regex = "\\w+@\\w+\\.[a-z]+(\\.[a-z]+)?"; + return Pattern.matches(regex, email); + } + + /** + * 验证身份证号码 + * + * @param idCard 居民身份证号码15位或18位,最后一位可能是数字或字母 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkIdCard(String idCard) { + String regex = "[1-9]\\d{13,16}[a-zA-Z0-9]{1}"; + return Pattern.matches(regex, idCard); + } + + /** + * 验证手机号码(支持国际格式,+86135xxxx...(中国内地),+00852137xxxx...(中国香港)) + * + * @param mobile 移动、联通、电信运营商的号码段 + *

移动的号段:134(0-8)、135、136、137、138、139、147(预计用于TD上网卡) + * 、150、151、152、157(TD专用)、158、159、187(未启用)、188(TD专用)

+ *

联通的号段:130、131、132、155、156(世界风专用)、185(未启用)、186(3g)

+ *

电信的号段:133、153、180(未启用)、189

+ * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkMobile(String mobile) { + String regex = "(\\+\\d+)?1[3458]\\d{9}$"; + return Pattern.matches(regex, mobile); + } + + /** + * 验证固定电话号码 + * + * @param phone 电话号码,格式:国家(地区)电话代码 + 区号(城市代码) + 电话号码,如:+8602085588447 + *

国家(地区) 代码 :标识电话号码的国家(地区)的标准国家(地区)代码。它包含从 0 到 9 的一位或多位数字, + * 数字之后是空格分隔的国家(地区)代码。

+ *

区号(城市代码):这可能包含一个或多个从 0 到 9 的数字,地区或城市代码放在圆括号—— + * 对不使用地区或城市代码的国家(地区),则省略该组件。

+ *

电话号码:这包含从 0 到 9 的一个或多个数字

+ * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkPhone(String phone) { + String regex = "(\\+\\d+)?(\\d{3,4}\\-?)?\\d{7,8}$"; + return Pattern.matches(regex, phone); + } + + /** + * 验证整数(正整数和负整数) + * + * @param digit 一位或多位0-9之间的整数 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkDigit(String digit) { + String regex = "\\-?[1-9]\\d+"; + return Pattern.matches(regex, digit); + } + + /** + * 验证整数和浮点数(正负整数和正负浮点数) + * + * @param decimals 一位或多位0-9之间的浮点数,如:1.23,233.30 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkDecimals(String decimals) { + String regex = "\\-?[1-9]\\d+(\\.\\d+)?"; + return Pattern.matches(regex, decimals); + } + + /** + * 验证空白字符 + * + * @param blankSpace 空白字符,包括:空格、\t、\n、\r、\f、\x0B + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkBlankSpace(String blankSpace) { + String regex = "\\s+"; + return Pattern.matches(regex, blankSpace); + } + + /** + * 验证中文 + * + * @param chinese 中文字符 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkChinese(String chinese) { + String regex = "^[\u4E00-\u9FA5]+$"; + return Pattern.matches(regex, chinese); + } + + /** + * 验证日期(年月日) + * + * @param birthday 日期,格式:1992-09-03,或1992.09.03 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkBirthday(String birthday) { + String regex = "[1-9]{4}([-./])\\d{1,2}\\1\\d{1,2}"; + return Pattern.matches(regex, birthday); + } + + /** + * 验证URL地址 + * + * @param url 格式:http://blog.csdn.net:80/xyang81/article/details/7705960? 或 http://www.csdn.net:80 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkURL(String url) { + String regex = "(https?://(w{3}\\.)?)?\\w+\\.\\w+(\\.[a-zA-Z]+)*(:\\d{1,5})?(/\\w*)*(\\??(.+=.*)?(&.+=.*)?)?"; + return Pattern.matches(regex, url); + } + + /** + * 匹配中国邮政编码 + * + * @param postcode 邮政编码 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkPostcode(String postcode) { + String regex = "[1-9]\\d{5}"; + return Pattern.matches(regex, postcode); + } + + /** + * 匹配IP地址(简单匹配,格式,如:192.168.1.1,127.0.0.1,没有匹配IP段的大小) + * + * @param ipAddress IPv4标准地址 + * @return 验证成功返回true,验证失败返回false + */ + public static boolean checkIpAddress(String ipAddress) { + if (ipAddress != null) { + String regex = "[1-9](\\d{1,2})?\\.(0|([1-9](\\d{1,2})?))\\.(0|([1-9](\\d{1,2})?))\\.(0|([1-9](\\d{1,2})?))"; + return Pattern.matches(regex, ipAddress); + } else { + return false; + } + } + +} diff --git a/app/src/main/java/com/xypower/wpywapp/tool/ShellUtils.java b/app/src/main/java/com/xypower/wpywapp/tool/ShellUtils.java new file mode 100644 index 0000000..83c1fed --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/ShellUtils.java @@ -0,0 +1,204 @@ +package com.xypower.wpywapp.tool; + +/** + * Created by Administrator on 15-4-14. + */ + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.List; + +/** + * ShellUtils + */ +public class ShellUtils { + + + public static final String COMMAND_SU = "su"; + public static final String COMMAND_SH = "sh"; + public static final String COMMAND_EXIT = "exit\n"; + public static final String COMMAND_LINE_END = "\n"; + + + private ShellUtils() { + throw new AssertionError(); + } + + + /** + * check whether has root permission + * + * @return + */ + public static boolean checkRootPermission() { + return execCommand("echo root", true, false).result == 0; + } + + + /** + * execute shell command, default return result msg + * + * @param command command + * @param isRoot whether need to run with root + * @return + * @see ShellUtils#execCommand(Object[], boolean, boolean) + */ + public static CommandResult execCommand(String command, boolean isRoot) { + return execCommand(new String[] {command}, isRoot, true); + } + + + /** + * execute shell commands, default return result msg + * + * @param commands command list + * @param isRoot whether need to run with root + * @return + * @see ShellUtils#execCommand(Object[], boolean, boolean) + */ + public static CommandResult execCommand(List commands, boolean isRoot) { + return execCommand(commands == null ? null : commands.toArray(new String[] {}),isRoot, true); + } + + + /** + * execute shell commands, default return result msg + * + * @param commands command array + * @param isRoot whether need to run with root + * @return + * @see ShellUtils#execCommand(Object[], boolean, boolean) + */ + public static CommandResult execCommand(String[] commands, boolean isRoot) { + return execCommand(commands, isRoot, true); + } + + + /** + * execute shell command + * + * @param command command + * @param isRoot whether need to run with root + * @param isNeedResultMsg whether need result msg + * @return + * @see ShellUtils#execCommand(Object[], boolean, boolean) + */ + public static CommandResult execCommand(String command, boolean isRoot, boolean isNeedResultMsg) { + return execCommand(new String[] {command}, isRoot, isNeedResultMsg); + } + + + /** + * execute shell commands + * + * @param commands command list + * @param isRoot whether need to run with root + * @param isNeedResultMsg whether need result msg + * @return + * @see ShellUtils#execCommand(Object[], boolean, boolean) + */ + public static CommandResult execCommand(List commands, boolean isRoot, boolean isNeedResultMsg) { + return execCommand(commands==null?null:commands.toArray(new String[] {}), isRoot, isNeedResultMsg); + } + + + /** + * execute shell commands + * + * @param commands command array + * @param isRoot whether need to run with root + * @param isNeedResultMsg whether need result msg + * @return + + + * + if isNeedResultMsg is false, {@link CommandResult#successMsg} is null and + * {@link CommandResult#errorMsg} is null. + + * + if {@link CommandResult#result} is -1, there maybe some excepiton. + + * + + + */ + public static CommandResult execCommand(Object[] commands, boolean isRoot, boolean isNeedResultMsg) { + int result = -1; + if (commands == null || commands.length == 0) { + return new CommandResult(result, null, null); + } + + + Process process = null; + BufferedReader successResult = null; + BufferedReader errorResult = null; + StringBuilder successMsg = null; + StringBuilder errorMsg = null; + + + DataOutputStream os = null; + try { + process = Runtime.getRuntime().exec(isRoot ? COMMAND_SU : COMMAND_SH); + os = new DataOutputStream(process.getOutputStream()); + for (Object command : commands) { + String strCommand= String.valueOf(command); + + if (strCommand == null) { + continue; + } +// donnot use os.writeBytes(strCommand), avoid chinese charset error + os.write(strCommand.getBytes()); + os.writeBytes(COMMAND_LINE_END); + os.flush(); + } + os.writeBytes(COMMAND_EXIT); + os.flush(); + + + result = process.waitFor(); +// get command result + if (isNeedResultMsg) { + successMsg = new StringBuilder(); + errorMsg = new StringBuilder(); + successResult = new BufferedReader(new InputStreamReader(process.getInputStream())); + errorResult = new BufferedReader(new InputStreamReader(process.getErrorStream())); + String s; + while ((s = successResult.readLine()) != null) { + successMsg.append(s); + } + while ((s = errorResult.readLine()) != null) { + errorMsg.append(s); + } + } + } catch (IOException e) { + e.printStackTrace(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (os != null) { + os.close(); + } + if (successResult != null) { + successResult.close(); + } + if (errorResult != null) { + errorResult.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + + + if (process != null) { + process.destroy(); + } + } + return new CommandResult(result, successMsg == null ? null : successMsg.toString(), errorMsg == null ? null + : errorMsg.toString()); + } + +} + diff --git a/app/src/main/java/com/xypower/wpywapp/tool/Utils.java b/app/src/main/java/com/xypower/wpywapp/tool/Utils.java new file mode 100644 index 0000000..041f2c7 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/Utils.java @@ -0,0 +1,337 @@ +///** +// * siir.es.adbWireless.adbWireless.java +// * +// * This program is free software: you can redistribute it and/or modify +// * it under the terms of the GNU General Public License as published by +// * the Free Software Foundation, either version 3 of the License, or +// * (at your option) any later version. +// * +// * This program is distributed in the hope that it will be useful, +// * but WITHOUT ANY WARRANTY; without even the implied warranty of +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// * GNU General Public License for more details. +// * +// * You should have received a copy of the GNU General Public License +// * along with this program. If not, see . +// * +// **/ +// +//package com.xypower.wpywapp.tool; +// +//import android.app.Activity; +//import android.app.AlertDialog; +//import android.app.Notification; +//import android.app.NotificationManager; +//import android.app.PendingIntent; +//import android.appwidget.AppWidgetManager; +//import android.content.ComponentName; +//import android.content.Context; +//import android.content.DialogInterface; +//import android.content.Intent; +//import android.content.SharedPreferences; +//import android.net.wifi.WifiInfo; +//import android.net.wifi.WifiManager; +//import android.os.PowerManager; +//import android.os.PowerManager.WakeLock; +//import android.preference.PreferenceManager; +//import android.widget.Toast; +// +// +//import java.io.BufferedReader; +//import java.io.DataOutputStream; +//import java.io.InputStreamReader; +// +//public class Utils { +// +// public static NotificationManager mNotificationManager; +// public static WakeLock mWakeLock; +// +// public static final int START_NOTIFICATION_ID = 1; +// public static final int ACTIVITY_SETTINGS = 2; +// +// +// +// @SuppressWarnings("deprecation") +// public static boolean adbStart(Context context) { +// try { +//// if (!adbWireless.USB_DEBUG) { +// Utils.setProp("service.adb.tcp.port", "5555"); +// try { +// if (Utils.isProcessRunning("adbd")) { +// Utils.runRootCommand("stop adbd"); +// } +// } catch (Exception e) { +// } +// Utils.runRootCommand("start adbd"); +//// } +// try { +// } catch (Exception e) { +// } +// SharedPreferences settings = context.getSharedPreferences("wireless", 0); +// SharedPreferences.Editor editor = settings.edit(); +// editor.putBoolean("mState", true); +// editor.commit(); +// +//// ComponentName cn = new ComponentName(context, adbWidgetProvider.class); +//// AppWidgetManager.getInstance(context).updateAppWidget(cn, adbWireless.remoteViews); +// +// // Try to auto connect +//// if (Utils.prefsAutoCon(context)) { +// Utils.autoConnect(context, "c"); +//// } +// +// // Wake Lock +//// if (Utils.prefsScreenOn(context)) { +//// final PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); +//// mWakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, context.getClass().getName()); +//// mWakeLock.acquire(); +//// } +// +// } catch (Exception e) { +// return false; +// } +// return true; +// } +// +// public static boolean adbStop(Context context) throws Exception { +// try { +//// if (!adbWireless.USB_DEBUG) { +//// if (adbWireless.mState) { +// setProp("service.adb.tcp.port", "-1"); +// runRootCommand("stop adbd"); +// runRootCommand("start adbd"); +//// } +//// } +// adbWireless.mState = false; +// +// SharedPreferences settings = context.getSharedPreferences("wireless", 0); +// SharedPreferences.Editor editor = settings.edit(); +// editor.putBoolean("mState", false); +// editor.commit(); +// +// ComponentName cn = new ComponentName(context, adbWidgetProvider.class); +// AppWidgetManager.getInstance(context).updateAppWidget(cn, adbWireless.remoteViews); +// +// // Try to auto disconnect +// if (Utils.prefsAutoCon(context)) { +// Utils.autoConnect(context, "d"); +// } +// +// // Wake Lock +// if(mWakeLock != null) { +// mWakeLock.release(); +// } +// +// if (Utils.mNotificationManager != null) { +// Utils.mNotificationManager.cancelAll(); +// } +// } catch (Exception e) { +// return false; +// } +// return true; +// +// } +// +// public static void autoConnect(Context context, String mode) { +// String autoConIP = Utils.prefsAutoConIP(context); +// String autoConPort = Utils.prefsAutoConPort(context); +// +// if (autoConIP.trim().equals("") || autoConPort.trim().equals("")) { +// return; +// } +// +// String urlRequest = "http://" + autoConIP + ":" + autoConPort + "/" + mode + "/" + Utils.getWifiIp(context); +// +// try { +//// new AutoConnectTask(urlRequest).execute(); +// } catch (Exception e) { +// } +// +// } +// +// public static boolean isProcessRunning(String processName) throws Exception { +// boolean running = false; +// Process process = null; +// process = Runtime.getRuntime().exec("ps"); +// BufferedReader in = new BufferedReader(new InputStreamReader(process.getInputStream())); +// String line = null; +// while ((line = in.readLine()) != null) { +// if (line.contains(processName)) { +// running = true; +// break; +// } +// } +// in.close(); +// process.waitFor(); +// return running; +// } +// +// public static boolean hasRootPermission() { +// Process process = null; +// DataOutputStream os = null; +// boolean rooted = true; +// try { +// process = Runtime.getRuntime().exec("su"); +// os = new DataOutputStream(process.getOutputStream()); +// os.writeBytes("exit\n"); +// os.flush(); +// process.waitFor(); +// if (process.exitValue() != 0) { +// rooted = false; +// } +// } catch (Exception e) { +// rooted = false; +// } finally { +// if (os != null) { +// try { +// os.close(); +// process.destroy(); +// } catch (Exception e) { +// } +// } +// } +// return rooted; +// } +// +// public static boolean runRootCommand(String command) { +// Process process = null; +// DataOutputStream os = null; +// try { +// process = Runtime.getRuntime().exec("su"); +// os = new DataOutputStream(process.getOutputStream()); +// os.writeBytes(command + "\n"); +// os.writeBytes("exit\n"); +// os.flush(); +// process.waitFor(); +// } catch (Exception e) { +// return false; +// } finally { +// try { +// if (os != null) { +// os.close(); +// } +// process.destroy(); +// } catch (Exception e) { +// } +// } +// return true; +// } +// +// public static boolean setProp(String property, String value) { +// return runRootCommand("setprop " + property + " " + value); +// } +// +// public static String getWifiIp(Context context) { +// WifiManager mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); +// int ip = mWifiManager.getConnectionInfo().getIpAddress(); +// return (ip & 0xFF) + "." + ((ip >> 8) & 0xFF) + "." + ((ip >> 16) & 0xFF) + "." + ((ip >> 24) & 0xFF); +// } +// +// public static void enableWiFi(Context context, boolean enable) { +// if (enable) { +//// Toast.makeText(context, R.string.turning_on_wifi, Toast.LENGTH_LONG).show(); +// } else { +//// Toast.makeText(context, R.string.turning_off_wifi, Toast.LENGTH_LONG).show(); +// } +// WifiManager mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); +// mWifiManager.setWifiEnabled(enable); +// } +// +// public static boolean checkWifiState(Context context) { +// try { +// WifiManager mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); +// WifiInfo wifiInfo = mWifiManager.getConnectionInfo(); +// if (!mWifiManager.isWifiEnabled() || wifiInfo.getSSID() == null) { +// return false; +// } +// +// return true; +// } catch (Exception e) { +// return false; +// } +// } +// +//// @SuppressWarnings("deprecation") +//// public static void showNotification(Context context, int icon, String text) { +//// final Notification notifyDetails = new Notification(icon, text, System.currentTimeMillis()); +//// notifyDetails.flags = Notification.FLAG_ONGOING_EVENT; +//// +//// if (prefsSound(context)) { +//// notifyDetails.defaults |= Notification.DEFAULT_SOUND; +//// } +//// +//// if (prefsVibrate(context)) { +//// notifyDetails.defaults |= Notification.DEFAULT_VIBRATE; +//// } +//// +//// Intent notifyIntent = new Intent(context, adbWireless.class); +//// notifyIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); +//// PendingIntent intent = PendingIntent.getActivity(context, 0, notifyIntent, 0); +//// notifyDetails.setLatestEventInfo(context, context.getResources().getString(R.string.noti_title), text, intent); +//// +//// if (Utils.mNotificationManager != null) { +//// Utils.mNotificationManager.notify(Utils.START_NOTIFICATION_ID, notifyDetails); +//// } else { +//// Utils.mNotificationManager = (NotificationManager) context.getSystemService(Activity.NOTIFICATION_SERVICE); +//// } +//// +//// Utils.mNotificationManager.notify(Utils.START_NOTIFICATION_ID, notifyDetails); +//// } +// +//// public static boolean prefsOnBoot(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_onboot_key), false); +//// } +//// +//// public static boolean prefsVibrate(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_vibrate_key), true); +//// } +//// +//// public static boolean prefsSound(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_sound_key), true); +//// } +//// +//// public static boolean prefsNoti(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_noti_key), true); +//// } +//// +//// public static boolean prefsHaptic(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_haptic_key), true); +//// } +//// +//// public static boolean prefsWiFiOn(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_wifi_on_key), false); +//// } +//// +//// public static boolean prefsWiFiOff(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_wifi_off_key), false); +//// +//// } +//// +//// public static boolean prefsAutoCon(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_autocon_key), false); +//// } +//// +//// public static String prefsAutoConIP(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getString(context.getResources().getString(R.string.pref_autoconip_key), ""); +//// } +//// +//// public static String prefsAutoConPort(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getString(context.getResources().getString(R.string.pref_autoconport_key), "8555"); +//// } +//// +//// public static boolean prefsScreenOn(Context context) { +//// SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(context); +//// return pref.getBoolean(context.getResources().getString(R.string.pref_screenon_key), false); +//// } +// +//} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/tool/testCmd.java b/app/src/main/java/com/xypower/wpywapp/tool/testCmd.java new file mode 100644 index 0000000..9ae3748 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/tool/testCmd.java @@ -0,0 +1,149 @@ +package com.xypower.wpywapp.tool; + + +import android.util.Log; + +import java.io.BufferedReader; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +public final class testCmd { + private static final String TAG = "RootCmd"; + private static boolean mHaveRoot = false; + /** + * 判断机器Android是否已经root,即是否获取root权限 + */ + public static boolean haveRoot() { + if (!mHaveRoot) { + int ret = execRootCmdSilent("echo test"); // 通过执行测试命令来检测 + if (ret != -1) { + mHaveRoot = true; + } else { + } + } else { + } + return mHaveRoot; + } + + /** + * 执行命令并且输出结果 + */ + public static String execRootCmd(String cmd) { + String result = ""; + DataOutputStream dos = null; + DataInputStream dis = null; + + try { + Process p = Runtime.getRuntime().exec("su"); + dos = new DataOutputStream(p.getOutputStream()); + dis = new DataInputStream(p.getInputStream()); + + Log.i(TAG, cmd); + dos.writeBytes(cmd + "\n"); + dos.flush(); + dos.writeBytes("exit\n"); + dos.flush(); + String line = null; + while ((line = dis.readLine()) != null) { + Log.d("result", line); + result += line; + } + p.waitFor(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (dos != null) { + try { + dos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + if (dis != null) { + try { + dis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return result; + } + + /** + * 执行命令但不关注结果输出 + */ + public static int execRootCmdSilent(String cmd) { + int result = -1; + DataOutputStream dos = null; + + try { + Process p = Runtime.getRuntime().exec("su"); + dos = new DataOutputStream(p.getOutputStream()); + + Log.i(TAG, cmd); + dos.writeBytes(cmd + "\n"); + dos.flush(); + dos.writeBytes("exit\n"); + dos.flush(); + p.waitFor(); + result = p.exitValue(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (dos != null) { + try { + dos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return result; + } + + public static int processBuilder() { + BufferedReader reader = null; + ProcessBuilder execBuilder = null; + execBuilder = new ProcessBuilder("sh", "-c", "cd proc/" + 6819); + execBuilder.redirectErrorStream(true); + Process exec = null; + try { + exec = execBuilder.start(); + + InputStream is = exec.getInputStream(); + reader = new BufferedReader(new InputStreamReader(is)); + + String line = reader.readLine(); + if (line == null) { + return 1; + } + } catch (IOException e) { + e.printStackTrace(); + } + + return 0; + } + + public static void runAdbCommand(String command) { + try { + Process process = Runtime.getRuntime().exec("su"); + DataOutputStream dataOutputStream = new DataOutputStream(process.getOutputStream()); + dataOutputStream.writeBytes(command + "\n"); + dataOutputStream.flush(); + dataOutputStream.writeBytes("exit\n"); + dataOutputStream.flush(); + int i = process.waitFor(); + System.out.println(i); + } catch (IOException | InterruptedException e) { + e.printStackTrace(); + } + } + +} + + + diff --git a/app/src/main/java/com/xypower/wpywapp/ui/BottomActivity.java b/app/src/main/java/com/xypower/wpywapp/ui/BottomActivity.java new file mode 100644 index 0000000..67c62bd --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/BottomActivity.java @@ -0,0 +1,57 @@ +package com.xypower.wpywapp.ui; + +import android.os.Bundle; +import android.view.View; + +import androidx.navigation.NavController; +import androidx.navigation.Navigation; +import androidx.navigation.ui.NavigationUI; + +import com.xypower.wpywapp.BR; +import com.xypower.wpywapp.R; +import com.xypower.wpywapp.base.BaseActivity; +import com.xypower.wpywapp.databinding.ActivityBottomBinding; +import com.xypower.wpywapp.interfaces.MainActCallback; +import com.xypower.wpywapp.page.DataBindingConfig; +import com.xypower.wpywapp.viewmoel.BottomViewModel; + +public class BottomActivity extends BaseActivity { + + private ActivityBottomBinding binding; + private BottomViewModel mState; + + @Override + protected void initViewModel() { + mState = getActivityScopeViewModel(BottomViewModel.class); + } + + @Override + protected DataBindingConfig getDataBindingConfig() { + return new DataBindingConfig(R.layout.activity_bottom, BR.vm, mState).addBindingParam(BR.callback, new MainActCallback() { + @Override + public void link(View view) { + + } + + @Override + public void getInfo(View view) { + + } + + @Override + public void takePic(View view) { + + } + }); + } + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + binding = (ActivityBottomBinding) getBinding(); + NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment_activity_bottom); + NavigationUI.setupWithNavController(binding.navView, navController); + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardFragment.java b/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardFragment.java new file mode 100644 index 0000000..001d48e --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardFragment.java @@ -0,0 +1,43 @@ +package com.xypower.wpywapp.ui.dashboard; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.xypower.wpywapp.BR; +import com.xypower.wpywapp.R; +import com.xypower.wpywapp.base.BaseFragment; +import com.xypower.wpywapp.page.DataBindingConfig; + +public class DashboardFragment extends BaseFragment { + + private DashboardViewModel mStates; + + @Override + protected void initViewModel() { + mStates = getFragmentScopeViewModel(DashboardViewModel.class); + } + + @Override + protected DataBindingConfig getDataBindingConfig() { + return new DataBindingConfig(R.layout.fragment_dashboard, BR.vm,mStates); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + //TODO tip 3: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化, + // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态, + //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 + + + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardViewModel.java b/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardViewModel.java new file mode 100644 index 0000000..b1a7069 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/dashboard/DashboardViewModel.java @@ -0,0 +1,11 @@ +package com.xypower.wpywapp.ui.dashboard; + +import androidx.lifecycle.ViewModel; + +public class DashboardViewModel extends ViewModel { + + + public DashboardViewModel() { + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/home/HomeFragment.java b/app/src/main/java/com/xypower/wpywapp/ui/home/HomeFragment.java new file mode 100644 index 0000000..2adf688 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/home/HomeFragment.java @@ -0,0 +1,41 @@ +package com.xypower.wpywapp.ui.home; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.xypower.wpywapp.BR; +import com.xypower.wpywapp.R; +import com.xypower.wpywapp.base.BaseFragment; +import com.xypower.wpywapp.page.DataBindingConfig; + +public class HomeFragment extends BaseFragment { + + private HomeViewModel mStates; + + @Override + protected void initViewModel() { + mStates = getFragmentScopeViewModel(HomeViewModel.class); + } + + @Override + protected DataBindingConfig getDataBindingConfig() { + return new DataBindingConfig(R.layout.fragment_home, BR.vm, mStates); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + //TODO tip 3: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化, + // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态, + //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/home/HomeViewModel.java b/app/src/main/java/com/xypower/wpywapp/ui/home/HomeViewModel.java new file mode 100644 index 0000000..34fb099 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/home/HomeViewModel.java @@ -0,0 +1,19 @@ +package com.xypower.wpywapp.ui.home; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class HomeViewModel extends ViewModel { + + private final MutableLiveData mText; + + public HomeViewModel() { + mText = new MutableLiveData<>(); + mText.setValue("This is home fragment"); + } + + public LiveData getText() { + return mText; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsFragment.java b/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsFragment.java new file mode 100644 index 0000000..cfe9924 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsFragment.java @@ -0,0 +1,40 @@ +package com.xypower.wpywapp.ui.notifications; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.xypower.wpywapp.BR; +import com.xypower.wpywapp.R; +import com.xypower.wpywapp.base.BaseFragment; +import com.xypower.wpywapp.page.DataBindingConfig; + +public class NotificationsFragment extends BaseFragment { + + private NotificationsViewModel mStates; + + @Override + protected void initViewModel() { + mStates = getFragmentScopeViewModel(NotificationsViewModel.class); + } + + @Override + protected DataBindingConfig getDataBindingConfig() { + return new DataBindingConfig(R.layout.fragment_notifications, BR.vm, mStates); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + //TODO tip 3: 从 PublishSubject 接收回推的数据,并在回调中响应数据的变化, + // 也即通过 BehaviorSubject(例如 ObservableField)通知控件属性重新渲染,并为其兜住最后一次状态, + //如这么说无体会,详见 https://xiaozhuanlan.com/topic/6741932805 + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsViewModel.java b/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsViewModel.java new file mode 100644 index 0000000..c2327e5 --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/ui/notifications/NotificationsViewModel.java @@ -0,0 +1,19 @@ +package com.xypower.wpywapp.ui.notifications; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class NotificationsViewModel extends ViewModel { + + private final MutableLiveData mText; + + public NotificationsViewModel() { + mText = new MutableLiveData<>(); + mText.setValue("This is notifications fragment"); + } + + public LiveData getText() { + return mText; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/xypower/wpywapp/viewmoel/BottomViewModel.java b/app/src/main/java/com/xypower/wpywapp/viewmoel/BottomViewModel.java new file mode 100644 index 0000000..5e5e83b --- /dev/null +++ b/app/src/main/java/com/xypower/wpywapp/viewmoel/BottomViewModel.java @@ -0,0 +1,19 @@ +package com.xypower.wpywapp.viewmoel; + +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; + +public class BottomViewModel extends ViewModel { + + private final MutableLiveData mText; + + public BottomViewModel() { + mText = new MutableLiveData<>(); + mText.setValue("This is notifications fragment"); + } + + public LiveData getText() { + return mText; + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dashboard_black_24dp.xml b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml new file mode 100644 index 0000000..46fc8de --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_home_black_24dp.xml b/app/src/main/res/drawable/ic_home_black_24dp.xml new file mode 100644 index 0000000..f8bb0b5 --- /dev/null +++ b/app/src/main/res/drawable/ic_home_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..78b75c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_bottom.xml b/app/src/main/res/layout/activity_bottom.xml new file mode 100644 index 0000000..da9c6d3 --- /dev/null +++ b/app/src/main/res/layout/activity_bottom.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..765af4e --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml new file mode 100644 index 0000000..eae66bb --- /dev/null +++ b/app/src/main/res/layout/fragment_home.xml @@ -0,0 +1,906 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml new file mode 100644 index 0000000..239842f --- /dev/null +++ b/app/src/main/res/layout/fragment_notifications.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml new file mode 100644 index 0000000..fb6d040 --- /dev/null +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/app/src/main/res/mipmap-anydpi/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/search.png b/app/src/main/res/mipmap-xxxhdpi/search.png new file mode 100644 index 0000000..b589386 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/search.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/takepic.png b/app/src/main/res/mipmap-xxxhdpi/takepic.png new file mode 100644 index 0000000..733a40b Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/takepic.png differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml new file mode 100644 index 0000000..9ca0780 --- /dev/null +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..fbb3170 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..c8524cd --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..e00c2dd --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 16dp + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..f79d9cc --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + Wpywapp + BottomActivity + 基本信息 + 视频 + 拍照 + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..78cb8af --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + +