├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ ├── helloworld.bsh │ └── toast.bsh │ ├── java │ └── jwf │ │ └── debugport │ │ └── app │ │ ├── App.java │ │ └── MainActivity.java │ └── res │ ├── layout │ └── main_activity.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lib-noop ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── jwf │ └── debugport │ ├── DebugPortService.java │ └── Params.java ├── lib ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── jwf │ │ └── debugport │ │ ├── DebugPortContentProvider.java │ │ ├── DebugPortService.java │ │ ├── Params.java │ │ ├── annotations │ │ └── Command.java │ │ └── internal │ │ ├── ClientConnection.java │ │ ├── CommandHelpInfo.java │ │ ├── TelnetServer.java │ │ ├── Utils.java │ │ ├── debug │ │ ├── DebugClientConnection.java │ │ ├── DebugTelnetServer.java │ │ ├── TelnetConsoleInterface.java │ │ └── commands │ │ │ ├── Commands.java │ │ │ ├── call.java │ │ │ ├── descriptors │ │ │ ├── FieldDescriptor.java │ │ │ ├── MemberDescriptor.java │ │ │ └── MethodDescriptor.java │ │ │ ├── exit.java │ │ │ ├── fields.java │ │ │ ├── fieldsLocal.java │ │ │ ├── get.java │ │ │ ├── help.java │ │ │ ├── methods.java │ │ │ ├── methodsLocal.java │ │ │ ├── set.java │ │ │ └── source.java │ │ └── sqlite │ │ ├── SQLiteClientConnection.java │ │ ├── SQLiteOpenHelper.java │ │ ├── SQLiteTelnetServer.java │ │ └── commands │ │ ├── Commands.java │ │ ├── CreateDatabaseCommand.java │ │ ├── DropDatabaseCommand.java │ │ ├── ExitCommand.java │ │ ├── HelpCommand.java │ │ ├── SQLiteCommand.java │ │ ├── ShowCreateTableCommand.java │ │ ├── ShowDatabasesCommand.java │ │ ├── ShowTablesCommand.java │ │ └── UseCommand.java │ └── res │ ├── drawable │ └── debugport_ic_notification.xml │ └── values │ ├── ids.xml │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .idea/ 10 | */build 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Jason Feinstein 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![](https://jitpack.io/v/jasonwyatt/Android-DebugPort.svg)](https://jitpack.io/#jasonwyatt/Android-DebugPort) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Android--DebugPort-green.svg?style=true)](https://android-arsenal.com/details/1/3540) [![License: Apache2.0](https://img.shields.io/badge/style-apache%202.0-blue.svg?style=flat&label=license)](http://www.apache.org/licenses/LICENSE-2.0) 2 | 3 | # Android DebugPort 4 | 5 | Android DebugPort is a drop-in utility which allows you to write and execute code within your app's context, at runtime, and from the comfort of your computer's terminal. Think of it as a window into your application through which you can both inspect _and_ modify its state. 6 | 7 | You can connect to one of two REPL servers running within your app: 8 | 9 | * Debug REPL - Run java-like code and inspect/modify the state of your android application. 10 | * SQLite REPL - Execute queries against your app's SQLite databaases. 11 | 12 | ## Getting Started - Drop-in 13 | 14 | ### Configure Your Dependencies 15 | 16 | Add the jitpack.io repository to your root `build.gradle`: 17 | 18 | ```groovy 19 | allprojects { 20 | repositories { 21 | jcenter() 22 | maven { url "https://jitpack.io" } 23 | } 24 | } 25 | ``` 26 | 27 | In your application's `build.gradle` file, add a dependency for Android DebugPort: 28 | 29 | ```groovy 30 | debugCompile 'com.github.jasonwyatt.Android-DebugPort:lib:2.1.0' 31 | releaseCompile 'com.github.jasonwyatt.Android-DebugPort:lib-noop:2.1.0' 32 | ``` 33 | 34 | **Note:** The final line above will use a [no-op version of the DebugPort library](https://github.com/jasonwyatt/Android-DebugPort-NOOP) in production builds. This makes it impossible for people to run the DebugPort server on a production build. 35 | 36 | ### Run Your App 37 | 38 | When you start your app after building for debug, you will see a low-priority notification in your system tray which will allow you to start the debugport servers. 39 | 40 | ### Connecting to the Debug Server 41 | 42 | $ telnet 192.168.2.83 8562 # on MacOS High Sierra: `nc 192.168.2.83 8562` 43 | Trying 192.168.2.83... 44 | Connected to 192.168.2.83. 45 | Escape character is '^]'. 46 | 47 | Android DebugPort v1.0 48 | Report issues at https://github.com/jasonwyatt/Android-DebugPort/issues 49 | 50 | BeanShell 2.0b6 - by Pat Niemeyer (pat@pat.net) 51 | bsh % 52 | 53 | There are a few built in commands, to see what they are, run `help();` 54 | 55 | bsh % help(); 56 | Available Commands: 57 | Access: 58 | call(Object obj, String method, Object... params) 59 | Call a method, regardless of access modifiers, on the provided object. 60 | get(Object obj, String fieldName) 61 | Get the value of a field, regardless of access modifiers, on the provided object. 62 | set(Object obj, String fieldName, Object value) 63 | Set the value of a field on the provided object to the given value, regardless of access modifiers. 64 | 65 | Field Inspection: 66 | fields(Class class) 67 | List all of the fields available for a particular class. 68 | fields(Object obj) 69 | List all of the fields available for a particular object. 70 | fieldsLocal(Class class) 71 | List all of the fields defined locally for a particular class. 72 | fieldsLocal(Object obj) 73 | List all of the fields defined locally for an object. 74 | 75 | Method Inspection: 76 | methods(Class class) 77 | Get the available methods for the provided class. 78 | methods(Object obj) 79 | Get the available methods for the provided object. 80 | methodsLocal(Class class) 81 | Show all of the locally-declared methods for the provided class. 82 | methodsLocal(Object obj) 83 | Show all of the locally-declared methods for the provided object. 84 | 85 | Other: 86 | exit() 87 | Exit this interpreter. 88 | help() 89 | Show this help message. 90 | source(String scriptPath) 91 | Load and run a Beanshell script within your app's assets folder. 92 | 93 | bsh % 94 | 95 | Also, your application variable is automatically included as a global variable in the interpreter. It's called `app`. Try running `methodsLocal(app);`: 96 | 97 | bsh % methodsLocal(app); 98 | declared methods: { 99 | public void onCreate() 100 | } 101 | bsh % 102 | 103 | Don't forget that you can execute whatever code you wish within the DebugPort. See the [beanshell documentation](http://beanshell.org/manual/contents.html) for the full rundown. 104 | 105 | You can exit at any time by running the `exit();` command. 106 | 107 | ### Connecting to the SQLite Server 108 | 109 | $ telnet 192.168.0.100 8563 # on MacOS High Sierra: `nc 192.168.2.83 8563` 110 | Trying 192.168.0.100... 111 | Connected to 192.168.0.100. 112 | Escape character is '^]'. 113 | 114 | Android DebugPort v1.0 115 | Report issues at https://github.com/jasonwyatt/Android-DebugPort/issues 116 | 117 | SQLite Database REPL 118 | 119 | sqlite> 120 | 121 | As with the Debug server, there is a help command for the SQLite server: 122 | 123 | sqlite> help; 124 | Help: 125 | As you'd expect, you can execute any valid SQLite statements against the database to which you're 126 | currently connected (see: `USE [database name];` below). 127 | 128 | In addition to regular SQLite commands, Android DebugPort provides additional functionality via several 129 | additional commands. 130 | 131 | Available non-SQLite commands (case insensitive): 132 | Databases: 133 | CREATE DATABASE [database name]; 134 | Create a new database called [database name]. 135 | DROP DATABASE [database name]; 136 | Drop the database named [database name] from the app's collection of databases. 137 | USE [database name]; 138 | Connect to the database called [database name]. All SQL commands will be executed against 139 | this database until USE is called again. 140 | 141 | Inspection: 142 | SHOW CREATE TABLE [table name]; 143 | Show the CREATE TABLE command used to create [table name]. 144 | SHOW DATABASES; 145 | Show all available databases for the app, including temporary databases. 146 | SHOW TABLES; 147 | Show all of the tables defined for the database to which you are currently connected. 148 | 149 | Other: 150 | exit; or quit; 151 | Exit this interpreter. 152 | help; 153 | Show this help message. 154 | 155 | sqlite> 156 | 157 | Try running `show databases;` to see the available databases for your app: 158 | 159 | sqlite> show databases; 160 | +----------+ 161 | | Database | 162 | +----------+ 163 | | blog | 164 | | projects | 165 | +----------+ 166 | 167 | sqlite> 168 | 169 | Run `use [database name];` to connect to a database, and once you're connected, you can run any SQLite command you want. You can quit at any time by running the `exit;` command. 170 | 171 | ## Advanced Configuration 172 | 173 | You can configure Android-DebugPort by setting any of the following `` values in your Application's `AndroidManifest.xml`. 174 | 175 | ```xml 176 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | ``` 191 | 192 | **Note:** It is recommended that if you wish to supply these meta-data values, you should consider setting them within an `AndroidManifest.xml` file for the `debug` build variant. 193 | 194 | ## License 195 | This library is released under the [Apache 2.0 License](https://github.com/jasonwyatt/Android-DebugPort/blob/master/LICENCE). 196 | 197 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 26 5 | buildToolsVersion '26.0.1' 6 | 7 | defaultConfig { 8 | applicationId "jwf.debugport.app" 9 | minSdkVersion 14 10 | targetSdkVersion 26 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | testCompile 'junit:junit:4.12' 25 | compile 'com.android.support:appcompat-v7:26.1.0' 26 | 27 | debugCompile project(':lib') 28 | releaseCompile project(':lib-noop') 29 | } 30 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jason/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/assets/helloworld.bsh: -------------------------------------------------------------------------------- 1 | print("hello world"); -------------------------------------------------------------------------------- /app/src/main/assets/toast.bsh: -------------------------------------------------------------------------------- 1 | import android.os.Handler; 2 | import android.os.Looper; 3 | import android.widget.Toast; 4 | 5 | showToast(text) { 6 | new Handler(Looper.getMainLooper()).post(new Runnable() { 7 | public void run() { 8 | Toast.makeText(app, text, Toast.LENGTH_LONG).show(); 9 | } 10 | }); 11 | } -------------------------------------------------------------------------------- /app/src/main/java/jwf/debugport/app/App.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.app; 2 | 3 | import android.app.Application; 4 | import android.content.Intent; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import jwf.debugport.DebugPortService; 10 | 11 | /** 12 | * 13 | */ 14 | public class App extends Application { 15 | HashMap mStringIntHashmap = new HashMap<>(); 16 | public Class[] mClassArray; 17 | public int[] mIntArray; 18 | public boolean[] mBooleanArray; 19 | public char[] mCharArray; 20 | public short[] mShortArray; 21 | public byte[] mByteArray; 22 | public long[] mLongArray; 23 | private String mSecretString = "This is a private value."; 24 | private int mSecretInt = 42; 25 | 26 | @Override 27 | public void onCreate() { 28 | super.onCreate(); 29 | mStringIntHashmap.put("Test", 1); 30 | } 31 | 32 | public Map getStringIntHashmap() { 33 | return mStringIntHashmap; 34 | } 35 | 36 | public void setStringIntHashmap(HashMap map) { 37 | mStringIntHashmap = map; 38 | } 39 | 40 | public int[] getIntArray() { 41 | return mIntArray; 42 | } 43 | 44 | public void setIntArray(int[] arry) { 45 | mIntArray = arry; 46 | } 47 | 48 | public void setIntArrayVarArg(int... arr) { 49 | mIntArray = arr; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/jwf/debugport/app/MainActivity.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.app; 2 | 3 | import android.content.Context; 4 | import android.net.wifi.WifiInfo; 5 | import android.net.wifi.WifiManager; 6 | import android.os.Bundle; 7 | import android.support.annotation.Nullable; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.view.View; 10 | import android.widget.CompoundButton; 11 | import android.widget.TextView; 12 | import android.widget.ToggleButton; 13 | 14 | import java.util.Locale; 15 | 16 | import jwf.debugport.DebugPortService; 17 | import jwf.debugport.Params; 18 | 19 | /** 20 | * 21 | */ 22 | public class MainActivity extends AppCompatActivity implements CompoundButton.OnCheckedChangeListener { 23 | @Override 24 | protected void onCreate(@Nullable Bundle savedInstanceState) { 25 | super.onCreate(savedInstanceState); 26 | 27 | setContentView(R.layout.main_activity); 28 | 29 | ToggleButton toggle = (ToggleButton) findViewById(R.id.server_toggle); 30 | if (toggle != null) { 31 | toggle.setOnCheckedChangeListener(this); 32 | } 33 | } 34 | 35 | @Override 36 | public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 37 | TextView debugStatus = (TextView) findViewById(R.id.debug_server_status); 38 | TextView sqliteStatus = (TextView) findViewById(R.id.sqlite_server_status); 39 | if (isChecked) { 40 | Params params = DebugPortService.start(this); 41 | if (debugStatus != null) { 42 | debugStatus.setText(getString(R.string.debug_server_status, getIpAddress(), params.getDebugPort())); 43 | debugStatus.setVisibility(View.VISIBLE); 44 | } 45 | if (sqliteStatus != null) { 46 | sqliteStatus.setText(getString(R.string.sqlite_server_status, getIpAddress(), params.getSQLitePort())); 47 | sqliteStatus.setVisibility(View.VISIBLE); 48 | } 49 | } else { 50 | DebugPortService.stop(this); 51 | if (debugStatus != null) { 52 | debugStatus.setVisibility(View.GONE); 53 | } 54 | if (sqliteStatus != null) { 55 | sqliteStatus.setVisibility(View.GONE); 56 | } 57 | } 58 | } 59 | 60 | public String getIpAddress() { 61 | WifiManager wifiMan = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); 62 | WifiInfo wifiInf = wifiMan.getConnectionInfo(); 63 | int ipAddress = wifiInf.getIpAddress(); 64 | return String.format(Locale.getDefault(), "%d.%d.%d.%d", (ipAddress & 0xff),(ipAddress >> 8 & 0xff),(ipAddress >> 16 & 0xff),(ipAddress >> 24 & 0xff)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 12 | 17 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android DebugPort Testapp 3 | stop server 4 | start server 5 | Debug server running at %1$s:%2$d 6 | SQLite server running at %1$s:%2$d 7 | 8 | import android.os.*; 9 | import java.util.*; 10 | x = 1+1; 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:2.3.3' 9 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | jcenter() 16 | maven { url "https://jitpack.io" } 17 | maven { url 'https://maven.google.com' } 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonwyatt/Android-DebugPort/ec7283ef78ab3f9bdf259ccf5e619fb55deabca6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 09 13:48:22 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /lib-noop/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | 4 | group='com.github.jasonwyatt' 5 | 6 | android { 7 | compileSdkVersion 26 8 | buildToolsVersion "26.0.1" 9 | 10 | defaultConfig { 11 | minSdkVersion 14 12 | targetSdkVersion 26 13 | versionCode 5 14 | versionName "2.1.0" 15 | } 16 | } 17 | 18 | dependencies { 19 | compile fileTree(dir: 'libs', include: ['*.jar']) 20 | testCompile 'junit:junit:4.12' 21 | } 22 | 23 | // build a jar with source files 24 | task sourcesJar(type: Jar) { 25 | from android.sourceSets.main.java.srcDirs 26 | classifier = 'sources' 27 | } 28 | 29 | task javadoc(type: Javadoc) { 30 | failOnError false 31 | source = android.sourceSets.main.java.sourceFiles 32 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 33 | classpath += configurations.compile 34 | } 35 | 36 | // build a jar with javadoc 37 | task javadocJar(type: Jar, dependsOn: javadoc) { 38 | classifier = 'javadoc' 39 | from javadoc.destinationDir 40 | } 41 | 42 | artifacts { 43 | archives sourcesJar 44 | archives javadocJar 45 | } 46 | -------------------------------------------------------------------------------- /lib-noop/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jason/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /lib-noop/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib-noop/src/main/java/jwf/debugport/DebugPortService.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport; 2 | 3 | import android.app.Service; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.os.IBinder; 7 | 8 | /** 9 | * No-op implementation of DebugPortService. 10 | */ 11 | public class DebugPortService extends Service { 12 | public static final String METADATA_DEBUG_PORT = "jwf.debugport.METADATA_DEBUG_PORT"; 13 | public static final String METADATA_SQLITE_PORT = "jwf.debugport.METADATA_SQLITE_PORT"; 14 | public static final String METADATA_STARTUP_COMMANDS = "jwf.debugport.METADATA_STARTUP_COMMANDS"; 15 | 16 | public static Params start(Context context) { 17 | Params params = new Params(); 18 | return params; 19 | } 20 | 21 | public static void start(Context context, Params params) { 22 | } 23 | 24 | public static void stop(Context context) { 25 | } 26 | 27 | public static void kill(Context context) { 28 | } 29 | 30 | @Override 31 | public IBinder onBind(Intent intent) { 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib-noop/src/main/java/jwf/debugport/Params.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | 6 | /** 7 | * Duplicate of Params class for no-op production usage. 8 | */ 9 | public class Params implements Parcelable { 10 | public static final int DEFAULT_ANDROID_PORT = -1; 11 | public static final int DEFAULT_SQLITE_PORT = -1; 12 | private int mDebugPort; 13 | private int mSQLitePort; 14 | private String[] mStartupCommands; 15 | 16 | public Params() { 17 | mDebugPort = DEFAULT_ANDROID_PORT; 18 | mSQLitePort = DEFAULT_SQLITE_PORT; 19 | mStartupCommands = new String[0]; 20 | } 21 | 22 | @Deprecated 23 | public Params setPort(int port) { 24 | return this; 25 | } 26 | 27 | @Deprecated 28 | public int getPort() { 29 | return mDebugPort; 30 | } 31 | 32 | public Params setDebugPort(int port) { 33 | return this; 34 | } 35 | 36 | public int getDebugPort() { 37 | return mDebugPort; 38 | } 39 | 40 | public Params setSQLitePort(int port) { 41 | return this; 42 | } 43 | 44 | public int getSQLitePort() { 45 | return mSQLitePort; 46 | } 47 | 48 | public Params setStartupCommands(String[] commands) { 49 | return this; 50 | } 51 | 52 | public String[] getStartupCommands() { 53 | return mStartupCommands; 54 | } 55 | 56 | @Override 57 | public int describeContents() { 58 | return hashCode(); 59 | } 60 | 61 | @Override 62 | public void writeToParcel(Parcel dest, int flags) { 63 | dest.writeInt(mDebugPort); 64 | dest.writeInt(mSQLitePort); 65 | dest.writeInt(mStartupCommands.length); 66 | dest.writeStringArray(mStartupCommands); 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "No-Op Params<>"; 72 | } 73 | 74 | public static final Creator CREATOR = new Creator() { 75 | @Override 76 | public Params createFromParcel(Parcel source) { 77 | Params p = new Params(); 78 | p.setDebugPort(source.readInt()); 79 | p.setSQLitePort(source.readInt()); 80 | 81 | int startupCommandsLength = source.readInt(); 82 | String[] startupCommands = new String[startupCommandsLength]; 83 | source.readStringArray(startupCommands); 84 | p.setStartupCommands(startupCommands); 85 | return p; 86 | } 87 | 88 | @Override 89 | public Params[] newArray(int size) { 90 | return new Params[size]; 91 | } 92 | }; 93 | } 94 | -------------------------------------------------------------------------------- /lib/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lib/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven' 3 | 4 | group='com.github.jasonwyatt' 5 | 6 | android { 7 | compileSdkVersion 26 8 | buildToolsVersion '26.0.1' 9 | 10 | defaultConfig { 11 | minSdkVersion 14 12 | targetSdkVersion 26 13 | versionCode 5 14 | versionName "2.1.0" 15 | } 16 | 17 | lintOptions { 18 | disable 'InvalidPackage' 19 | } 20 | } 21 | 22 | dependencies { 23 | compile "org.apache-extras.beanshell:bsh:2.0b6" 24 | compile "com.android.support:support-compat:26.1.0" 25 | } 26 | 27 | // build a jar with source files 28 | task sourcesJar(type: Jar) { 29 | from android.sourceSets.main.java.srcDirs 30 | classifier = 'sources' 31 | } 32 | 33 | task javadoc(type: Javadoc) { 34 | failOnError false 35 | source = android.sourceSets.main.java.sourceFiles 36 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 37 | classpath += configurations.compile 38 | } 39 | 40 | // build a jar with javadoc 41 | task javadocJar(type: Jar, dependsOn: javadoc) { 42 | classifier = 'javadoc' 43 | from javadoc.destinationDir 44 | } 45 | 46 | artifacts { 47 | archives sourcesJar 48 | archives javadocJar 49 | } 50 | -------------------------------------------------------------------------------- /lib/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/jason/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /lib/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/DebugPortContentProvider.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | 8 | /** 9 | * Simple ContentProvider-based approach to initializing the DebugPort service. 10 | * @author jason 11 | */ 12 | 13 | public class DebugPortContentProvider extends ContentProvider { 14 | @Override 15 | public boolean onCreate() { 16 | DebugPortService.initialize(getContext()); 17 | return true; 18 | } 19 | 20 | @Override 21 | public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { 22 | return null; 23 | } 24 | 25 | @Override 26 | public String getType(Uri uri) { 27 | return null; 28 | } 29 | 30 | @Override 31 | public Uri insert(Uri uri, ContentValues values) { 32 | return null; 33 | } 34 | 35 | @Override 36 | public int delete(Uri uri, String selection, String[] selectionArgs) { 37 | return 0; 38 | } 39 | 40 | @Override 41 | public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { 42 | return 0; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/DebugPortService.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport; 2 | 3 | import android.app.Notification; 4 | import android.app.NotificationChannel; 5 | import android.app.NotificationManager; 6 | import android.app.PendingIntent; 7 | import android.app.Service; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.pm.ApplicationInfo; 11 | import android.content.pm.PackageManager; 12 | import android.content.res.Resources; 13 | import android.os.AsyncTask; 14 | import android.os.Build; 15 | import android.os.Bundle; 16 | import android.os.IBinder; 17 | import android.os.PowerManager; 18 | import android.support.annotation.NonNull; 19 | import android.support.annotation.Nullable; 20 | import android.support.v4.app.NotificationCompat; 21 | import android.util.Log; 22 | 23 | import jwf.debugport.internal.debug.DebugTelnetServer; 24 | import jwf.debugport.internal.TelnetServer; 25 | import jwf.debugport.internal.Utils; 26 | import jwf.debugport.internal.sqlite.SQLiteTelnetServer; 27 | 28 | /** 29 | * 30 | */ 31 | public class DebugPortService extends Service { 32 | private static final String TAG = "DebugPortService"; 33 | private static final String NOTIFICATION_CHANNEL = "androiddebugport"; 34 | public static final String METADATA_DEBUG_PORT = "jwf.debugport.METADATA_DEBUG_PORT"; 35 | public static final String METADATA_SQLITE_PORT = "jwf.debugport.METADATA_SQLITE_PORT"; 36 | public static final String METADATA_STARTUP_COMMANDS = "jwf.debugport.METADATA_STARTUP_COMMANDS"; 37 | private static final String INTENT_EXTRA_PARAMS = "jwf.debugport.PARAMS"; 38 | private static final String ACTION_KILL = "jwf.debugport.ACTION_KILL"; 39 | private static final String ACTION_STOP = "jwf.debugport.ACTION_STOP"; 40 | private static final String ACTION_START = "jwf.debugport.ACTION_START"; 41 | private static final String ACTION_INITIALIZE = "jwf.debugport.ACTION_INIT"; 42 | private static final int STOP_REQUEST_CODE = 0; 43 | private static final int START_REQUEST_CODE = 1; 44 | private static final int NOTIFICATION_ID = R.id.debugport_notification_id; 45 | private TelnetServer mDebugServer; 46 | private PowerManager.WakeLock mWakeLock; 47 | private SQLiteTelnetServer mSQLiteServer; 48 | private Params mParams; 49 | private boolean mServersStarted; 50 | 51 | /** 52 | * Utility method to start the DebugPortService 53 | * @return Params object generated by looking at manifest metadata. 54 | */ 55 | public static Params start(Context context) { 56 | Params params = getManifestParams(context); 57 | start(context, params); 58 | return params; 59 | } 60 | 61 | /** 62 | * Utility method to start the DebugPortService. 63 | * @param params Parameters to configure the service. 64 | */ 65 | public static void start(Context context, @NonNull Params params) { 66 | Intent intent = new Intent(context, DebugPortService.class); 67 | intent.setAction(ACTION_START); 68 | intent.putExtra(INTENT_EXTRA_PARAMS, params); 69 | context.startService(intent); 70 | } 71 | 72 | /** 73 | * Stop the currently-running server. 74 | */ 75 | public static void stop(Context context) { 76 | Intent intent = new Intent(context, DebugPortService.class); 77 | intent.setAction(ACTION_STOP); 78 | context.startService(intent); 79 | } 80 | 81 | /** 82 | * Kill the service. 83 | */ 84 | public static void kill(Context context) { 85 | Intent intent = new Intent(context, DebugPortService.class); 86 | context.stopService(intent); 87 | } 88 | 89 | private void stopServers() { 90 | if (mDebugServer != null) { 91 | mDebugServer.killServer(); 92 | mDebugServer = null; 93 | } 94 | if (mSQLiteServer != null) { 95 | mSQLiteServer.killServer(); 96 | mSQLiteServer = null; 97 | } 98 | if (mWakeLock != null) { 99 | mWakeLock.release(); 100 | mWakeLock = null; 101 | } 102 | mServersStarted = false; 103 | showNotification(); 104 | } 105 | 106 | private void startServers(Params params) { 107 | if (mServersStarted) { 108 | stopServers(); 109 | } 110 | 111 | mParams = params; 112 | mServersStarted = true; 113 | 114 | PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE); 115 | mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DebugPortWakeLock"); 116 | mWakeLock.acquire(); 117 | 118 | new AsyncTask() { 119 | @SuppressWarnings("deprecation") 120 | @Override 121 | protected void onPostExecute(Void res) { 122 | showNotification(); 123 | } 124 | 125 | @Override 126 | protected Void doInBackground(Params... params) { 127 | try { 128 | mDebugServer = new DebugTelnetServer(DebugPortService.this, params[0]); 129 | mDebugServer.startServer(); 130 | mSQLiteServer = new SQLiteTelnetServer(DebugPortService.this, params[0]); 131 | mSQLiteServer.startServer(); 132 | } catch (java.io.IOException e) { 133 | throw new RuntimeException(e); 134 | } 135 | return null; 136 | } 137 | }.execute(params); 138 | } 139 | 140 | private void showNotification() { 141 | startForeground(NOTIFICATION_ID, buildNotification(mParams)); 142 | } 143 | 144 | @Override 145 | public int onStartCommand(Intent intent, int flags, int startId) { 146 | if (ACTION_STOP.equals(intent.getAction())) { 147 | stopServers(); 148 | } else if (ACTION_START.equals(intent.getAction())) { 149 | Params params = intent.getParcelableExtra(INTENT_EXTRA_PARAMS); 150 | startServers(params); 151 | } else if (ACTION_INITIALIZE.equals(intent.getAction())) { 152 | showNotification(); 153 | } else if (ACTION_KILL.equals(intent.getAction())) { 154 | kill(this); 155 | return START_NOT_STICKY; 156 | } else { 157 | return START_NOT_STICKY; 158 | } 159 | return START_STICKY; 160 | } 161 | 162 | @Override 163 | public void onDestroy() { 164 | super.onDestroy(); 165 | stopServers(); 166 | stopForeground(true); 167 | } 168 | 169 | private Notification buildNotification(@Nullable Params params) { 170 | NotificationManager mgr = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); 171 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 172 | mgr.createNotificationChannel(new NotificationChannel(NOTIFICATION_CHANNEL, getString(R.string.debugport_notification_channel_name), NotificationManager.IMPORTANCE_MIN)); 173 | } 174 | 175 | if (params == null) { 176 | params = getManifestParams(this); 177 | } 178 | 179 | String ip = Utils.getIpAddress(this); 180 | String message; 181 | String summary; 182 | if (mServersStarted) { 183 | message = getString(R.string.debugport_notification_subtitle_running, ip, params.getDebugPort(), params.getSQLitePort()); 184 | summary = getString(R.string.debugport_notification_summary_running, ip, params.getDebugPort(), params.getSQLitePort()); 185 | } else { 186 | message = getString(R.string.debugport_notification_subtitle_stopped); 187 | summary = getString(R.string.debugport_notification_summary_stopped); 188 | } 189 | 190 | NotificationCompat.Builder builder = new NotificationCompat.Builder(this); 191 | builder.setSmallIcon(R.drawable.debugport_ic_notification); 192 | int appLabel = getApplicationInfo().labelRes; 193 | if (appLabel == 0) { 194 | builder.setContentTitle(getString(R.string.debugport_notification_title_plain)); 195 | } else { 196 | builder.setContentTitle(getString(R.string.debugport_notification_title, getString(appLabel))); 197 | } 198 | builder.setStyle(new NotificationCompat.BigTextStyle().bigText(message).setSummaryText(summary)); 199 | builder.setPriority(NotificationCompat.PRIORITY_MIN); 200 | builder.setContentText(message); 201 | builder.addAction(mServersStarted ? buildStopAction() : buildStartAction(params)); 202 | builder.addAction(buildKillAction()); 203 | builder.setChannelId(NOTIFICATION_CHANNEL); 204 | 205 | return builder.build(); 206 | } 207 | 208 | private NotificationCompat.Action buildKillAction() { 209 | Intent intent = new Intent(this, DebugPortService.class); 210 | intent.setAction(ACTION_KILL); 211 | 212 | PendingIntent pIntent = PendingIntent.getService(this, STOP_REQUEST_CODE, intent, 0); 213 | 214 | return new NotificationCompat.Action.Builder(0, getString(R.string.debugport_notification_action_kill), pIntent).build(); 215 | } 216 | 217 | private NotificationCompat.Action buildStopAction() { 218 | Intent intent = new Intent(this, DebugPortService.class); 219 | intent.setAction(ACTION_STOP); 220 | 221 | PendingIntent pIntent = PendingIntent.getService(this, STOP_REQUEST_CODE, intent, 0); 222 | 223 | return new NotificationCompat.Action.Builder(0, getString(R.string.debugport_notification_action_stop), pIntent) 224 | .build(); 225 | } 226 | 227 | private NotificationCompat.Action buildStartAction(@NonNull Params params) { 228 | Intent intent = new Intent(this, DebugPortService.class); 229 | intent.setAction(ACTION_START); 230 | intent.putExtra(INTENT_EXTRA_PARAMS, params); 231 | 232 | PendingIntent pIntent = PendingIntent.getService(this, START_REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT); 233 | 234 | return new NotificationCompat.Action.Builder(0, getString(R.string.debugport_notification_action_start), pIntent) 235 | .build(); 236 | } 237 | 238 | @Override 239 | public IBinder onBind(Intent intent) { 240 | return null; 241 | } 242 | 243 | /** 244 | * Called by {@link DebugPortContentProvider} to initialize the service. 245 | */ 246 | static void initialize(Context context) { 247 | Intent intent = new Intent(context, DebugPortService.class); 248 | intent.setAction(ACTION_INITIALIZE); 249 | context.startService(intent); 250 | } 251 | 252 | /** 253 | * Load parameters from the manifest metadata. 254 | */ 255 | private static Params getManifestParams(Context context) { 256 | Params params = new Params(); 257 | 258 | try { 259 | Context app = context.getApplicationContext(); 260 | ApplicationInfo ai = app.getPackageManager().getApplicationInfo(app.getPackageName(), PackageManager.GET_META_DATA); 261 | Bundle bundle = ai.metaData; 262 | 263 | params.setDebugPort(bundle.getInt(METADATA_DEBUG_PORT, params.getDebugPort())); 264 | params.setSQLitePort(bundle.getInt(METADATA_SQLITE_PORT, params.getSQLitePort())); 265 | 266 | int startupCommandsResId = bundle.getInt(METADATA_STARTUP_COMMANDS, 0); 267 | if (startupCommandsResId != 0) { 268 | try { 269 | params.setStartupCommands(app.getResources().getStringArray(startupCommandsResId)); 270 | } catch (Resources.NotFoundException nfe) { 271 | Log.w(TAG, "Error getting startup commands. Using empty array of commands instead.", nfe); 272 | } 273 | } 274 | } catch (PackageManager.NameNotFoundException | NullPointerException e) { 275 | Log.w(TAG, "Error getting metadata, using default parameters.", e); 276 | } 277 | 278 | return params; 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/Params.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport; 2 | 3 | import android.os.Parcel; 4 | import android.os.Parcelable; 5 | import android.support.annotation.Nullable; 6 | 7 | /** 8 | * Configuration parameters for the {@link DebugPortService} 9 | */ 10 | public class Params implements Parcelable { 11 | public static final int DEFAULT_ANDROID_PORT = 8562; 12 | public static final int DEFAULT_SQLITE_PORT = 8563; 13 | private int mDebugPort; 14 | private int mSQLitePort; 15 | private String[] mStartupCommands; 16 | 17 | public Params() { 18 | mDebugPort = DEFAULT_ANDROID_PORT; 19 | mSQLitePort = DEFAULT_SQLITE_PORT; 20 | mStartupCommands = new String[0]; 21 | } 22 | 23 | /** 24 | * Set the port on which the {@link jwf.debugport.internal.TelnetServer} should be made available. 25 | * @deprecated Use {@link #setDebugPort(int)} instead. 26 | */ 27 | @Deprecated 28 | public Params setPort(int port) { 29 | mDebugPort = port; 30 | return this; 31 | } 32 | 33 | /** 34 | * Get the port on which the {@link jwf.debugport.internal.TelnetServer} will be made available. 35 | * @deprecated Use {@link #getDebugPort()} instead. 36 | */ 37 | @Deprecated 38 | public int getPort() { 39 | return mDebugPort; 40 | } 41 | 42 | /** 43 | * Set the port on which the debug port server will be made available. 44 | */ 45 | public Params setDebugPort(int port) { 46 | mDebugPort = port; 47 | return this; 48 | } 49 | 50 | /** 51 | * Get the port on which the debug port server will be made available. 52 | */ 53 | public int getDebugPort() { 54 | return mDebugPort; 55 | } 56 | 57 | /** 58 | * Set the port on which the SQLite-context server will be made available. 59 | */ 60 | public Params setSQLitePort(int port) { 61 | mSQLitePort = port; 62 | return this; 63 | } 64 | 65 | /** 66 | * Get the port on which the SQLite-context server will be made available. 67 | */ 68 | public int getSQLitePort() { 69 | return mSQLitePort; 70 | } 71 | 72 | /** 73 | * Set an array of commands which should be executed on the interpreter before the telnet client 74 | * is given control. This can be useful if you need to run a bunch of import 75 | * statements, or configure some state. 76 | */ 77 | public Params setStartupCommands(@Nullable String[] commands) { 78 | if (commands == null) { 79 | commands = new String[0]; 80 | } 81 | mStartupCommands = commands; 82 | return this; 83 | } 84 | 85 | /** 86 | * Get the startup commands which will be executed on the interpreter before each telnet client 87 | * is given control. 88 | */ 89 | public String[] getStartupCommands() { 90 | return mStartupCommands; 91 | } 92 | 93 | @Override 94 | public int describeContents() { 95 | return hashCode(); 96 | } 97 | 98 | @Override 99 | public void writeToParcel(Parcel dest, int flags) { 100 | dest.writeInt(mDebugPort); 101 | dest.writeInt(mSQLitePort); 102 | dest.writeInt(mStartupCommands.length); 103 | dest.writeStringArray(mStartupCommands); 104 | } 105 | 106 | @Override 107 | public String toString() { 108 | return "Params"; 109 | } 110 | 111 | public static final Creator CREATOR = new Creator() { 112 | @Override 113 | public Params createFromParcel(Parcel source) { 114 | Params p = new Params(); 115 | p.setDebugPort(source.readInt()); 116 | p.setSQLitePort(source.readInt()); 117 | 118 | int startupCommandsLength = source.readInt(); 119 | String[] startupCommands = new String[startupCommandsLength]; 120 | source.readStringArray(startupCommands); 121 | p.setStartupCommands(startupCommands); 122 | return p; 123 | } 124 | 125 | @Override 126 | public Params[] newArray(int size) { 127 | return new Params[size]; 128 | } 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/annotations/Command.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.annotations; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * 10 | */ 11 | @Target(ElementType.TYPE) 12 | @Retention(RetentionPolicy.RUNTIME) 13 | public @interface Command { 14 | String GROUP_METHOD_INSPECTION = "Method Inspection"; 15 | String GROUP_FIELD_INSPECTION = "Field Inspection"; 16 | String GROUP_ACCESS = "Access"; 17 | String GROUP_SQL_INSPECTION = "Inspection"; 18 | String GROUP_SQL_DATABASES = "Databases"; 19 | String GROUP_OTHER = "Other"; 20 | 21 | String group() default GROUP_OTHER; 22 | 23 | @Target({ElementType.METHOD, ElementType.TYPE}) 24 | @Retention(RetentionPolicy.RUNTIME) 25 | @interface Help { 26 | String value() default ""; 27 | String group() default GROUP_OTHER; 28 | String format() default ""; 29 | } 30 | 31 | @Target(ElementType.PARAMETER) 32 | @Retention(RetentionPolicy.RUNTIME) 33 | @interface ParamName { 34 | String value() default ""; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/ClientConnection.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.util.Log; 6 | 7 | import java.io.IOException; 8 | import java.lang.ref.WeakReference; 9 | import java.net.Socket; 10 | 11 | /** 12 | * Base ClientConnection type. 13 | */ 14 | public abstract class ClientConnection implements Runnable { 15 | private static final String TAG = "ClientConnection"; 16 | private final Application mApp; 17 | private final Socket mSocket; 18 | private final WeakReference mParent; 19 | private final String[] mStartupCommands; 20 | private String mLogTag; 21 | 22 | public ClientConnection(Context context, Socket client, TelnetServer parent, String[] startupCommands) { 23 | mApp = (Application) context.getApplicationContext(); 24 | mSocket = client; 25 | mParent = new WeakReference(parent); 26 | mStartupCommands = startupCommands; 27 | } 28 | 29 | public Application getApp() { 30 | return mApp; 31 | } 32 | 33 | public Socket getSocket() { 34 | return mSocket; 35 | } 36 | 37 | public TelnetServer getParent() { 38 | return mParent.get(); 39 | } 40 | 41 | public String[] getStartupCommands() { 42 | return mStartupCommands; 43 | } 44 | 45 | public void close() { 46 | logInfo("Client closing:" + getSocket().toString()); 47 | TelnetServer parent = getParent(); 48 | if (parent != null) { 49 | parent.notifyClosing(this); 50 | } 51 | try { 52 | closeConnection(); 53 | getSocket().close(); 54 | } catch (IOException e) { 55 | // m'eh.. 56 | logError("Error upon closing.", e); 57 | } 58 | } 59 | 60 | protected void logInfo(String msg) { 61 | Log.i(getTag(), msg); 62 | } 63 | 64 | protected void logDebug(String msg) { 65 | Log.d(getTag(), msg); 66 | } 67 | 68 | protected void logError(String msg) { 69 | Log.e(getTag(), msg); 70 | } 71 | 72 | protected void logError(String msg, Throwable t) { 73 | Log.e(getTag(), msg, t); 74 | } 75 | 76 | private String getTag() { 77 | if (mLogTag == null) { 78 | mLogTag = getClass().getSimpleName(); 79 | } 80 | return mLogTag; 81 | } 82 | 83 | public abstract void closeConnection(); 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/CommandHelpInfo.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.lang.annotation.Annotation; 6 | import java.lang.reflect.Method; 7 | import java.util.StringTokenizer; 8 | 9 | import jwf.debugport.annotations.Command; 10 | 11 | /** 12 | * Created by jason on 5/15/16. 13 | */ 14 | public class CommandHelpInfo implements Comparable { 15 | private final Class mCommandClass; 16 | private Command.Help mHelpAnnotation; 17 | private Command mCommandAnnotation; 18 | 19 | public CommandHelpInfo(Class commandClass) { 20 | mCommandClass = commandClass; 21 | if (commandClass.isAnnotationPresent(Command.class)) { 22 | mCommandAnnotation = (Command) commandClass.getAnnotation(Command.class); 23 | } 24 | if (commandClass.isAnnotationPresent(Command.Help.class)) { 25 | mHelpAnnotation = (Command.Help) commandClass.getAnnotation(Command.Help.class); 26 | } 27 | } 28 | 29 | public String getCommandGroup() { 30 | if (mHelpAnnotation != null && !TextUtils.isEmpty(mHelpAnnotation.group())) { 31 | return mHelpAnnotation.group(); 32 | } 33 | if (mCommandAnnotation != null && !TextUtils.isEmpty(mCommandAnnotation.group())) { 34 | return mCommandAnnotation.group(); 35 | } 36 | return Command.GROUP_OTHER; 37 | } 38 | 39 | public void appendHelpInfoForMethods(StringBuilder target, int nameStartCol, int helpStartCol, int maxCols) { 40 | Method[] methods = mCommandClass.getMethods(); 41 | boolean isFirst = true; 42 | for (Method method : methods) { 43 | if (!method.getName().equals("invoke") || !method.isAnnotationPresent(Command.Help.class)) { 44 | continue; 45 | } 46 | 47 | // separate each item with a new line. 48 | if (!isFirst) { 49 | target.append("\n"); 50 | } 51 | isFirst = false; 52 | 53 | target.append(Utils.spaces(nameStartCol)); 54 | String signature = getSignature(method); 55 | target.append(signature); 56 | target.append("\n"); 57 | 58 | appendHelpText(target, method.getAnnotation(Command.Help.class).value(), helpStartCol, maxCols); 59 | } 60 | } 61 | 62 | public void appendHelpInfoForClass(StringBuilder target, int nameStartCol, int helpStartCol, int maxCols) { 63 | target.append(Utils.spaces(nameStartCol)); 64 | if (!TextUtils.isEmpty(mHelpAnnotation.format())) { 65 | target.append(mHelpAnnotation.format()); 66 | } else { 67 | target.append(mCommandClass.getSimpleName()); 68 | } 69 | target.append("\n"); 70 | appendHelpText(target, mHelpAnnotation.value(), helpStartCol, maxCols); 71 | } 72 | 73 | String getSignature(Method method) { 74 | Annotation[][] paramAnnotations = method.getParameterAnnotations(); 75 | Class[] params = method.getParameterTypes(); 76 | StringBuilder signatureBuilder = new StringBuilder(); 77 | signatureBuilder.append(mCommandClass.getSimpleName()); 78 | signatureBuilder.append("("); 79 | for (int j = 2; j < params.length; j++) { 80 | // first two params are Interpreter and CallStack, ignore those. 81 | if (j != 2) { 82 | signatureBuilder.append(", "); 83 | } 84 | 85 | String name = params[j].getSimpleName(); 86 | if (j == params.length - 1 && method.isVarArgs() && params[j].isArray()) { 87 | signatureBuilder.append(name.replace("[]", "")); 88 | signatureBuilder.append("..."); 89 | } else { 90 | signatureBuilder.append(name); 91 | } 92 | 93 | Annotation[] annotations = paramAnnotations[j]; 94 | Command.ParamName paramAnnotation = null; 95 | for (Annotation a : annotations) { 96 | if (a instanceof Command.ParamName) { 97 | paramAnnotation = (Command.ParamName) a; 98 | break; 99 | } 100 | } 101 | if (paramAnnotation != null) { 102 | if (!TextUtils.isEmpty(paramAnnotation.value())) { 103 | signatureBuilder.append(" "); 104 | signatureBuilder.append(paramAnnotation.value()); 105 | } 106 | } 107 | } 108 | signatureBuilder.append(")"); 109 | return signatureBuilder.toString(); 110 | } 111 | 112 | public static void appendHelpText(StringBuilder target, String helpText, int helpStartCol, int maxCols) { 113 | int cols = helpStartCol; 114 | target.append(Utils.spaces(helpStartCol)); 115 | 116 | StringTokenizer tokens = new StringTokenizer(helpText, " "); 117 | while (tokens.hasMoreTokens()) { 118 | String token = tokens.nextToken(); 119 | target.append(token); 120 | if (tokens.hasMoreTokens()) { 121 | target.append(" "); 122 | } 123 | cols += token.length() + 1; 124 | if (cols >= maxCols && tokens.hasMoreTokens()) { 125 | cols = helpStartCol; 126 | target.append("\n"); 127 | target.append(Utils.spaces(helpStartCol)); 128 | } 129 | } 130 | } 131 | 132 | @SuppressWarnings("NullableProblems") 133 | @Override 134 | public int compareTo(CommandHelpInfo another) { 135 | if (another == null) { 136 | return -1; 137 | } 138 | String myGroup = getCommandGroup(); 139 | String theirGroup = another.getCommandGroup(); 140 | int groupCompare = myGroup.compareTo(theirGroup); 141 | if (groupCompare != 0) { 142 | // "Other" group should always come last. 143 | if (myGroup.equals(Command.GROUP_OTHER)) { 144 | return 1; 145 | } 146 | if (theirGroup.equals(Command.GROUP_OTHER)) { 147 | return - 1; 148 | } 149 | return groupCompare; 150 | } 151 | return mCommandClass.getSimpleName().compareTo(another.mCommandClass.getSimpleName()); 152 | } 153 | 154 | public Class getCommandClass() { 155 | return mCommandClass; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/TelnetServer.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal; 2 | 3 | import android.content.Context; 4 | import android.util.Log; 5 | 6 | import java.io.IOException; 7 | import java.io.PrintWriter; 8 | import java.net.ServerSocket; 9 | import java.net.Socket; 10 | import java.net.UnknownHostException; 11 | import java.util.HashSet; 12 | 13 | import jwf.debugport.BuildConfig; 14 | import jwf.debugport.Params; 15 | 16 | 17 | /** 18 | * 19 | */ 20 | public abstract class TelnetServer implements Runnable { 21 | private static final String TAG = "TelnetServer"; 22 | private final Object mLock = new Object(); 23 | private final Context mApp; 24 | private final Params mParams; 25 | private final int mPort; 26 | private ServerSocket mServerSocket; 27 | private Thread mThread; 28 | private volatile boolean mAlive = false; 29 | private HashSet mClients = new HashSet<>(); 30 | 31 | public TelnetServer(Context app, Params params, int port) throws UnknownHostException { 32 | mApp = app; 33 | mParams = params; 34 | mPort = port; 35 | } 36 | 37 | public void startServer() throws IOException { 38 | synchronized (mLock) { 39 | if (mServerSocket != null && mServerSocket.isBound()) { 40 | throw new IOException("server already started"); 41 | } 42 | } 43 | mThread = new Thread(this); 44 | mThread.start(); 45 | } 46 | 47 | public void killServer() { 48 | if (mThread != null) { 49 | mThread.interrupt(); 50 | } 51 | mAlive = false; 52 | synchronized (mLock) { 53 | for (ClientConnection client : mClients) { 54 | client.close(); 55 | } 56 | try { 57 | mServerSocket.close(); 58 | } catch (IOException e) { 59 | // m'eh 60 | } 61 | mServerSocket = null; 62 | } 63 | } 64 | 65 | @Override 66 | public void run() { 67 | try { 68 | mServerSocket = new ServerSocket(mPort); 69 | } catch (IOException e) { 70 | throw new RuntimeException(e); 71 | } 72 | mAlive = true; 73 | Log.i(getTag(), "Server running at "+ Utils.getIpAddress(mApp)+":"+mPort); 74 | while (mAlive) { 75 | Socket client; 76 | T clientConn; 77 | if (mServerSocket == null) { 78 | break; 79 | } 80 | try { 81 | client = mServerSocket.accept(); 82 | PrintWriter out = new PrintWriter(client.getOutputStream()); 83 | out.println(); 84 | out.println("Android DebugPort v" + BuildConfig.VERSION_NAME); 85 | out.println("Report issues at https://github.com/jasonwyatt/Android-DebugPort/issues"); 86 | out.println(); 87 | out.flush(); 88 | } catch (IOException e) { 89 | if (e.getMessage().equals("Socket closed")) { 90 | // no big deal, we are done here.. 91 | break; 92 | } 93 | Log.w(getTag(), "An error occurred accepting a client connection.", e); 94 | continue; 95 | } 96 | 97 | synchronized (mLock) { 98 | clientConn = getClientConnection(mApp, client, this, mParams.getStartupCommands()); 99 | mClients.add(clientConn); 100 | } 101 | new Thread(clientConn).start(); 102 | } 103 | Log.i(getTag(), "Shutdown."); 104 | } 105 | 106 | public abstract T getClientConnection(Context c, Socket socket, TelnetServer server, String[] startupCommands); 107 | 108 | public void notifyClosing(T clientConnection) { 109 | if (!mAlive) { 110 | // we are dead anyway, no need to worry.. 111 | return; 112 | } 113 | 114 | synchronized (mLock) { 115 | mClients.remove(clientConnection); 116 | } 117 | } 118 | 119 | private String getTag() { 120 | return getClass().getSimpleName(); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/Utils.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.Context; 5 | import android.database.Cursor; 6 | import android.net.wifi.WifiInfo; 7 | import android.net.wifi.WifiManager; 8 | 9 | import java.io.PrintWriter; 10 | import java.util.Locale; 11 | 12 | /** 13 | * 14 | */ 15 | public class Utils { 16 | public static final int INDENT_SPACES = 2; 17 | 18 | @SuppressLint("DefaultLocale") 19 | public static String getIpAddress(Context context) { 20 | WifiManager wifiMan = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 21 | WifiInfo wifiInf = wifiMan.getConnectionInfo(); 22 | int ipAddress = wifiInf.getIpAddress(); 23 | return String.format("%d.%d.%d.%d", (ipAddress & 0xff),(ipAddress >> 8 & 0xff),(ipAddress >> 16 & 0xff),(ipAddress >> 24 & 0xff)); 24 | } 25 | 26 | public static void printTable(PrintWriter out, String[] headers, Object[][] data) { 27 | int[] longest = new int[headers.length]; 28 | for (int column = 0; column < headers.length; column++) { 29 | longest[column] = headers[column].length(); 30 | } 31 | for (int row = 0; row < data.length; row++) { 32 | for (int column = 0; column < data[row].length; column++) { 33 | String dataStr = data[row][column].toString(); 34 | if (dataStr.length() > longest[column]) { 35 | longest[column] = dataStr.length(); 36 | } 37 | } 38 | } 39 | 40 | int gutter = 1; 41 | 42 | printTableDivider(out, longest, gutter); 43 | printTableRow(out, longest, gutter, headers); 44 | printTableDivider(out, longest, gutter); 45 | for (int row = 0; row < data.length; row++) { 46 | printTableRow(out, longest, gutter, data[row]); 47 | } 48 | printTableDivider(out, longest, gutter); 49 | } 50 | 51 | private static void printTableRow(PrintWriter out, int[] columnWidths, int columnGutterSize, Object[] data) { 52 | out.print("|"); 53 | for (int i = 0; i < data.length; i++) { 54 | out.print(multStr(" ", columnGutterSize)); 55 | String dataStr = data[i].toString(); 56 | out.print(dataStr); 57 | out.print(multStr(" ", columnGutterSize + (columnWidths[i] - dataStr.length()))); 58 | out.print("|"); 59 | } 60 | out.println(); 61 | } 62 | 63 | private static void printTableDivider(PrintWriter out, int[] columnWidths, int columnGutterSize) { 64 | out.print("+"); 65 | for (int i = 0; i < columnWidths.length; i++) { 66 | out.print(multStr("-", columnWidths[i] + columnGutterSize * 2)); 67 | out.print("+"); 68 | } 69 | out.println(); 70 | } 71 | 72 | public static String multStr(String s, int times) { 73 | StringBuilder sb = new StringBuilder(s.length() * times); 74 | for (int i = 0; i < times; i++) { 75 | sb.append(s); 76 | } 77 | return sb.toString(); 78 | } 79 | 80 | public static void printTable(PrintWriter out, Cursor c) { 81 | int columns = c.getColumnCount(); 82 | if (columns == 0) { 83 | return; 84 | } 85 | String[] columnNames = c.getColumnNames(); 86 | int gutter = 1; 87 | int[] longest = new int[columns]; 88 | for (int column = 0; column < columns; column++) { 89 | longest[column] = columnNames[column].length(); 90 | } 91 | int row = 0; 92 | while (c.moveToNext()) { 93 | for (int column = 0; column < columns; column++) { 94 | String dataStr = c.isNull(column) ? "null" : c.getString(column); 95 | if (dataStr.length() > longest[column]) { 96 | longest[column] = dataStr.length(); 97 | } 98 | } 99 | } 100 | 101 | printTableDivider(out, longest, gutter); 102 | printTableRow(out, longest, gutter, columnNames); 103 | printTableDivider(out, longest, gutter); 104 | 105 | // let's rewind to actually print the contents. 106 | boolean hasFirst = c.moveToFirst(); 107 | 108 | 109 | if (hasFirst) { 110 | do { 111 | String[] data = new String[columns]; 112 | for (int i = 0; i < columns; i++) { 113 | data[i] = c.isNull(i) ? "NULL" : c.getString(i); 114 | } 115 | printTableRow(out, longest, gutter, data); 116 | } while (c.moveToNext()); 117 | } 118 | 119 | printTableDivider(out, longest, gutter); 120 | } 121 | 122 | public static String nanosToMillis(long nanos) { 123 | float millis = nanos / 1000000.0f; 124 | return String.format(Locale.getDefault(), "%.3fms", millis); 125 | } 126 | 127 | public static String spaces(int spaces) { 128 | return multStr(" ", spaces); 129 | } 130 | 131 | public static String indent(int indentations) { 132 | return spaces(indentations * INDENT_SPACES); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/DebugClientConnection.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.IOException; 6 | import java.net.Socket; 7 | 8 | import bsh.Interpreter; 9 | import jwf.debugport.internal.debug.commands.Commands; 10 | import jwf.debugport.internal.ClientConnection; 11 | import jwf.debugport.internal.TelnetServer; 12 | 13 | /** 14 | * 15 | */ 16 | public class DebugClientConnection extends ClientConnection implements Commands.ExitListener { 17 | private TelnetConsoleInterface mConsole; 18 | 19 | public DebugClientConnection(Context context, Socket client, TelnetServer parent, String[] startupCommands) { 20 | super(context, client, parent, startupCommands); 21 | } 22 | 23 | @Override 24 | public void closeConnection() { 25 | // nothing to do here.. 26 | } 27 | 28 | @Override 29 | public void run() { 30 | try { 31 | mConsole = new TelnetConsoleInterface(getSocket().getInputStream(), getSocket().getOutputStream()); 32 | Interpreter interpreter = new Interpreter(mConsole); 33 | interpreter.setShowResults(true); 34 | interpreter.set("cmd", new Commands(this)); 35 | interpreter.set("app", getApp()); 36 | interpreter.eval("importCommands(\"jwf.debugport.internal.debug.commands\")"); 37 | 38 | for (String startupCommand : getStartupCommands()) { 39 | interpreter.eval(startupCommand); 40 | } 41 | 42 | interpreter.setExitOnEOF(false); 43 | interpreter.run(); 44 | } catch (Exception e) { 45 | logError("Error: ", e); 46 | } finally { 47 | close(); 48 | } 49 | } 50 | 51 | @Override 52 | public void onExit() { 53 | try { 54 | mConsole.close(); 55 | } catch (IOException e) { 56 | // m'eh 57 | } 58 | close(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/DebugTelnetServer.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug; 2 | 3 | import android.content.Context; 4 | 5 | import java.net.Socket; 6 | import java.net.UnknownHostException; 7 | 8 | import jwf.debugport.Params; 9 | import jwf.debugport.internal.TelnetServer; 10 | 11 | /** 12 | * Implementation of {@link TelnetServer} for a REPL in the Application context. 13 | */ 14 | public class DebugTelnetServer extends TelnetServer { 15 | public DebugTelnetServer(Context app, Params params) throws UnknownHostException { 16 | super(app, params, params.getDebugPort()); 17 | } 18 | 19 | @Override 20 | public DebugClientConnection getClientConnection(Context c, Socket socket, TelnetServer server, String[] startupCommands) { 21 | return new DebugClientConnection(c, socket, server, startupCommands); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/TelnetConsoleInterface.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.io.InputStreamReader; 7 | import java.io.OutputStream; 8 | import java.io.PrintStream; 9 | import java.io.Reader; 10 | 11 | import bsh.ConsoleInterface; 12 | 13 | /** 14 | * 15 | */ 16 | class TelnetConsoleInterface implements ConsoleInterface, Closeable { 17 | private final InputStream mIn; 18 | private final OutputStream mOut; 19 | private InputStreamReader mReader; 20 | private PrintStream mPrintStream; 21 | 22 | public TelnetConsoleInterface(InputStream inputStream, OutputStream outputStream) { 23 | mIn = inputStream; 24 | mOut = outputStream; 25 | } 26 | 27 | @Override 28 | public Reader getIn() { 29 | if (mReader == null) { 30 | mReader = new InputStreamReader(mIn); 31 | } 32 | return mReader; 33 | } 34 | 35 | @Override 36 | public PrintStream getOut() { 37 | if (mPrintStream == null) { 38 | mPrintStream = new PrintStream(mOut, true); 39 | } 40 | return mPrintStream; 41 | } 42 | 43 | @Override 44 | public PrintStream getErr() { 45 | return getOut(); 46 | } 47 | 48 | @Override 49 | public void println(Object o) { 50 | getOut().println(o == null ? "null" : o.toString()); 51 | } 52 | 53 | @Override 54 | public void print(Object o) { 55 | getOut().print(o == null ? "null" : o.toString()); 56 | } 57 | 58 | @Override 59 | public void error(Object o) { 60 | getErr().print(o == null ? "null" : o.toString()); 61 | } 62 | 63 | @Override 64 | public void close() throws IOException { 65 | mIn.close(); 66 | mOut.close(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/Commands.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import java.lang.ref.WeakReference; 4 | 5 | /** 6 | * 7 | */ 8 | public final class Commands { 9 | private final WeakReference mListener; 10 | 11 | static { 12 | // helper block to initialize all the commands... TODO: maybe a better way to do this? 13 | help.registerCommand(help.class); 14 | help.registerCommand(exit.class); 15 | help.registerCommand(methods.class); 16 | help.registerCommand(methodsLocal.class); 17 | help.registerCommand(fields.class); 18 | help.registerCommand(fieldsLocal.class); 19 | help.registerCommand(get.class); 20 | help.registerCommand(set.class); 21 | help.registerCommand(call.class); 22 | help.registerCommand(source.class); 23 | } 24 | 25 | public Commands(ExitListener listener) { 26 | mListener = new WeakReference<>(listener); 27 | } 28 | 29 | public void exit() { 30 | ExitListener listener = mListener.get(); 31 | if (listener != null) { 32 | listener.onExit(); 33 | } 34 | } 35 | 36 | public interface ExitListener { 37 | void onExit(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/call.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.lang.reflect.Method; 5 | 6 | import bsh.CallStack; 7 | import bsh.Interpreter; 8 | import jwf.debugport.annotations.Command; 9 | 10 | /** 11 | * 12 | */ 13 | @Command(group = Command.GROUP_ACCESS) 14 | public class call { 15 | /* 16 | * In order to support a varargs-like behavior when the command is called with no third 17 | * parameter, we have to supply several methods.. beanshell doesn't really support varargs well. 18 | */ 19 | public static Object invoke( 20 | Interpreter interpreter, 21 | CallStack callStack, 22 | Object object, 23 | String methodName) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 24 | return invokeInner(object, methodName, null); 25 | } 26 | 27 | public static Object invoke( 28 | Interpreter interpreter, 29 | CallStack callStack, 30 | Object object, 31 | String methodName, 32 | Object p1) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 33 | return invokeInner(object, methodName, p1); 34 | } 35 | 36 | public static Object invoke( 37 | Interpreter interpreter, 38 | CallStack callStack, 39 | Object object, 40 | String methodName, 41 | Object p1, 42 | Object p2) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 43 | return invokeInner(object, methodName, p1, p2); 44 | } 45 | 46 | public static Object invoke( 47 | Interpreter interpreter, 48 | CallStack callStack, 49 | Object object, 50 | String methodName, 51 | Object p1, 52 | Object p2, 53 | Object p3) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 54 | return invokeInner(object, methodName, p1, p2, p3); 55 | } 56 | 57 | public static Object invoke( 58 | Interpreter interpreter, 59 | CallStack callStack, 60 | Object object, 61 | String methodName, 62 | Object p1, 63 | Object p2, 64 | Object p3, 65 | Object p4) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 66 | return invokeInner(object, methodName, p1, p2, p3, p4); 67 | } 68 | 69 | public static Object invoke( 70 | Interpreter interpreter, 71 | CallStack callStack, 72 | Object object, 73 | String methodName, 74 | Object p1, 75 | Object p2, 76 | Object p3, 77 | Object p4, 78 | Object p5) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 79 | return invokeInner(object, methodName, p1, p2, p3, p4, p5); 80 | } 81 | 82 | public static Object invoke( 83 | Interpreter interpreter, 84 | CallStack callStack, 85 | Object object, 86 | String methodName, 87 | Object p1, 88 | Object p2, 89 | Object p3, 90 | Object p4, 91 | Object p5, 92 | Object p6) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { 93 | return invokeInner(object, methodName, p1, p2, p3, p4, p5, p6); 94 | } 95 | 96 | @Command.Help("Call a method, regardless of access modifiers, on the provided object.") 97 | public static Object invoke( 98 | Interpreter interpreter, 99 | CallStack callStack, 100 | @Command.ParamName("obj") Object object, 101 | @Command.ParamName("method") String methodName, 102 | @Command.ParamName("params") Object... params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 103 | return invokeInner(object, methodName, params); 104 | } 105 | 106 | private static Object invokeInner( 107 | Object object, 108 | String methodName, 109 | Object... params) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 110 | Method method; 111 | Class[] paramClasses = new Class[0]; 112 | if (params != null) { 113 | paramClasses = new Class[params.length]; 114 | for (int i = 0; i < params.length; i++) { 115 | paramClasses[i] = params[i].getClass(); 116 | } 117 | } 118 | 119 | try { 120 | method = object.getClass().getDeclaredMethod(methodName, paramClasses); 121 | } catch (NoSuchMethodException e) { 122 | // try a method on a parent of the object's class 123 | method = object.getClass().getMethod(methodName, paramClasses); 124 | } 125 | method.setAccessible(true); 126 | return method.invoke(object, params); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/descriptors/FieldDescriptor.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands.descriptors; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.lang.reflect.Field; 6 | import java.lang.reflect.ParameterizedType; 7 | import java.lang.reflect.Type; 8 | import java.util.Arrays; 9 | 10 | /** 11 | * 12 | */ 13 | public class FieldDescriptor extends MemberDescriptor { 14 | private final Field mField; 15 | 16 | public FieldDescriptor(Field field) { 17 | super(field); 18 | mField = field; 19 | } 20 | 21 | @Override 22 | public String toString() { 23 | StringBuilder sb = new StringBuilder(); 24 | 25 | String modifier = getModifierString(); 26 | if (!TextUtils.isDigitsOnly(modifier)) { 27 | sb.append(modifier); 28 | sb.append(" "); 29 | } 30 | 31 | Type type = mField.getType(); 32 | sb.append(getSimpleClassName(type)); 33 | try { 34 | Type genericType = mField.getGenericType(); 35 | if (genericType instanceof ParameterizedType) { 36 | sb.append(getParametrizedTypeString((ParameterizedType) genericType)); 37 | } 38 | } catch (Exception e) { 39 | // nothing to do here. 40 | } 41 | sb.append(" "); 42 | sb.append(getName()); 43 | 44 | return sb.toString(); 45 | } 46 | 47 | public static FieldDescriptor[] fromFields(Field[] fields) { 48 | FieldDescriptor[] result = new FieldDescriptor[fields.length]; 49 | for (int i = 0; i < result.length; i++) { 50 | result[i] = new FieldDescriptor(fields[i]); 51 | } 52 | Arrays.sort(result); 53 | return result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/descriptors/MemberDescriptor.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands.descriptors; 2 | 3 | import java.lang.reflect.GenericArrayType; 4 | import java.lang.reflect.Member; 5 | import java.lang.reflect.Modifier; 6 | import java.lang.reflect.ParameterizedType; 7 | import java.lang.reflect.Type; 8 | import java.util.regex.Matcher; 9 | import java.util.regex.Pattern; 10 | 11 | /** 12 | * 13 | */ 14 | public class MemberDescriptor implements Comparable { 15 | private final int mModifiers; 16 | private final Class mClass; 17 | private final String mName; 18 | 19 | public MemberDescriptor(Member obj) { 20 | mModifiers = obj.getModifiers(); 21 | mClass = obj.getDeclaringClass(); 22 | mName = obj.getName(); 23 | } 24 | 25 | public String getModifierString() { 26 | return Modifier.toString(mModifiers); 27 | } 28 | 29 | public String getName() { 30 | return mName; 31 | } 32 | 33 | public String getClassName() { 34 | return mClass.getSimpleName(); 35 | } 36 | 37 | public int compareVisibility(MemberDescriptor another) { 38 | int visibilityMask = Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED; 39 | int myVisibility = mModifiers & visibilityMask; 40 | int theirVisibility = another.mModifiers & visibilityMask; 41 | 42 | if (myVisibility < theirVisibility) { 43 | return -1; 44 | } 45 | if (myVisibility > theirVisibility) { 46 | return 1; 47 | } 48 | 49 | return 0; 50 | } 51 | 52 | @SuppressWarnings("NullableProblems") 53 | @Override 54 | public int compareTo(MemberDescriptor another) { 55 | if (another == null) { 56 | return -1; 57 | } 58 | // first sort by staticness... 59 | if (!Modifier.isStatic(mModifiers) && Modifier.isStatic(another.mModifiers)) { 60 | return -1; 61 | } else if (Modifier.isStatic(mModifiers) && !Modifier.isStatic(another.mModifiers)) { 62 | return 1; 63 | } 64 | 65 | // then by visibility 66 | int visibilityComparison = compareVisibility(another); 67 | if (visibilityComparison != 0) { 68 | return visibilityComparison; 69 | } 70 | 71 | // then by name 72 | return mName.compareTo(another.mName); 73 | } 74 | 75 | public static String getParametrizedTypeString(ParameterizedType type) { 76 | StringBuilder sb = new StringBuilder(); 77 | Type[] typeArgs = type.getActualTypeArguments(); 78 | sb.append("<"); 79 | for (int i = 0; i < typeArgs.length; i++) { 80 | if (i != 0) { 81 | sb.append(", "); 82 | } 83 | sb.append(getSimpleClassName(typeArgs[i])); 84 | } 85 | sb.append(">"); 86 | return sb.toString(); 87 | } 88 | 89 | private static final Pattern sTypeSimpleClassPattern = Pattern.compile("([@a-z]+ )?([a-zA-Z0-9_]*\\.)*([a-zA-Z0-9_\\$]+)"); 90 | private static final Pattern sTypeArraySimpleClassPattern = Pattern.compile("([@a-z]+ )?\\[L([a-zA-Z0-9_]*\\.)*([a-zA-Z0-9_\\$]+);?"); 91 | public static String getSimpleClassName(Type type) { 92 | if (type instanceof ParameterizedType) { 93 | ParameterizedType pType = (ParameterizedType) type; 94 | StringBuilder sb = new StringBuilder(); 95 | sb.append(getSimpleClassName(pType.getRawType())); 96 | Type[] args = pType.getActualTypeArguments(); 97 | if (args.length > 0) { 98 | sb.append("<"); 99 | for (int i = 0; i < args.length; i++) { 100 | if (i != 0) { 101 | sb.append(", "); 102 | } 103 | sb.append(getSimpleClassName(args[i])); 104 | } 105 | sb.append(">"); 106 | } 107 | return sb.toString(); 108 | } 109 | if (type instanceof GenericArrayType) { 110 | GenericArrayType gType = (GenericArrayType) type; 111 | return getSimpleClassName(gType.getGenericComponentType()); 112 | } 113 | 114 | Matcher matcher = sTypeSimpleClassPattern.matcher(type.toString()); 115 | if (matcher.matches()) { 116 | String result = matcher.group(3); 117 | return result.replaceAll("\\$", "."); 118 | } 119 | matcher = sTypeArraySimpleClassPattern.matcher(type.toString()); 120 | if (matcher.matches()) { 121 | // must be an array.. 122 | String result = matcher.group(3); 123 | return result.replaceAll("\\$", ".") + "[]"; 124 | } 125 | 126 | // handle primitive others.. 127 | if (type instanceof Class) { 128 | Class c = (Class) type; 129 | StringBuilder sb = new StringBuilder(); 130 | sb.append(c.getComponentType().toString()); 131 | if (c.isArray()) { 132 | sb.append("[]"); 133 | } 134 | return sb.toString(); 135 | } 136 | // fallback 137 | return type.toString(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/descriptors/MethodDescriptor.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands.descriptors; 2 | 3 | import android.text.TextUtils; 4 | 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Type; 7 | import java.util.Arrays; 8 | 9 | /** 10 | * 11 | */ 12 | public class MethodDescriptor extends MemberDescriptor { 13 | private final Method mMethod; 14 | 15 | public MethodDescriptor(Method method) { 16 | super(method); 17 | mMethod = method; 18 | } 19 | 20 | @Override 21 | public String toString() { 22 | StringBuilder sb = new StringBuilder(); 23 | 24 | String modifiers = getModifierString(); 25 | if (!TextUtils.isEmpty(modifiers)) { 26 | sb.append(modifiers); 27 | sb.append(" "); 28 | } 29 | 30 | try { 31 | Type genericReturnType = mMethod.getGenericReturnType(); 32 | sb.append(getSimpleClassName(genericReturnType)); 33 | sb.append(" "); 34 | } catch (Exception e) { 35 | Class returnType = mMethod.getReturnType(); 36 | if (returnType.equals(Void.TYPE)) { 37 | sb.append("void "); 38 | } else { 39 | sb.append(returnType.getSimpleName()); 40 | sb.append(" "); 41 | } 42 | } 43 | 44 | sb.append(getName()); 45 | sb.append("("); 46 | try { 47 | Class[] params = mMethod.getParameterTypes(); 48 | Type[] genericParams = mMethod.getGenericParameterTypes(); 49 | for (int i = 0; i < genericParams.length; i++) { 50 | if (i != 0) { 51 | sb.append(", "); 52 | } 53 | Type paramType = genericParams[i]; 54 | 55 | String name = getSimpleClassName(paramType); 56 | if (i == genericParams.length-1 && mMethod.isVarArgs() && params[i].isArray()) { 57 | sb.append(name.replace("[]", "")); 58 | sb.append("..."); 59 | } else { 60 | sb.append(name); 61 | } 62 | } 63 | } catch (Exception e) { 64 | Class[] params = mMethod.getParameterTypes(); 65 | for (int i = 0; i < params.length; i++) { 66 | if (i != 0) { 67 | sb.append(", "); 68 | } 69 | String name = params[i].getSimpleName(); 70 | if (i == params.length-1 && mMethod.isVarArgs() && params[i].isArray()) { 71 | sb.append(name.replace("[]", "")); 72 | sb.append("..."); 73 | } else { 74 | sb.append(name); 75 | } 76 | } 77 | } 78 | sb.append(")"); 79 | 80 | return sb.toString(); 81 | } 82 | 83 | public static MethodDescriptor[] fromMethods(Method[] methods) { 84 | MethodDescriptor[] result = new MethodDescriptor[methods.length]; 85 | for (int i = 0; i < result.length; i++) { 86 | result[i] = new MethodDescriptor(methods[i]); 87 | } 88 | Arrays.sort(result); 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/exit.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import bsh.CallStack; 4 | import bsh.EvalError; 5 | import bsh.Interpreter; 6 | import jwf.debugport.annotations.Command; 7 | 8 | /** 9 | * 10 | */ 11 | @Command 12 | public class exit { 13 | @Command.Help("Exit this interpreter.") 14 | public static void invoke(Interpreter env, CallStack callStack) { 15 | env.println("Thanks for using Android DebugPort!"); 16 | try { 17 | ((Commands)env.get("cmd")).exit(); 18 | } catch (EvalError evalError) { 19 | env.error(evalError.toString()); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/fields.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import bsh.CallStack; 4 | import bsh.Interpreter; 5 | import jwf.debugport.annotations.Command; 6 | import jwf.debugport.internal.Utils; 7 | import jwf.debugport.internal.debug.commands.descriptors.FieldDescriptor; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_FIELD_INSPECTION) 13 | public class fields { 14 | @Command.Help("List all of the fields available for a particular object.") 15 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("obj") Object obj) { 16 | invoke(interpreter, callStack, obj.getClass()); 17 | } 18 | 19 | @Command.Help("List all of the fields available for a particular class.") 20 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("class") Class klass) { 21 | if (klass == null) { 22 | interpreter.println("value is null"); 23 | return; 24 | } 25 | FieldDescriptor[] fields = FieldDescriptor.fromFields(klass.getFields()); 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.append("fields {\n"); 29 | 30 | for (FieldDescriptor field : fields) { 31 | sb.append(Utils.indent(1)); 32 | sb.append(field.toString()); 33 | sb.append("\n"); 34 | } 35 | 36 | sb.append("}"); 37 | 38 | interpreter.println(sb.toString()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/fieldsLocal.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import bsh.CallStack; 4 | import bsh.Interpreter; 5 | import jwf.debugport.annotations.Command; 6 | import jwf.debugport.internal.Utils; 7 | import jwf.debugport.internal.debug.commands.descriptors.FieldDescriptor; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_FIELD_INSPECTION) 13 | public class fieldsLocal { 14 | @Command.Help("List all of the fields defined locally for an object.") 15 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("obj") Object obj) { 16 | invoke(interpreter, callStack, obj.getClass()); 17 | } 18 | 19 | @Command.Help("List all of the fields defined locally for a particular class.") 20 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("class") Class klass) { 21 | if (klass == null) { 22 | interpreter.println("null"); 23 | return; 24 | } 25 | FieldDescriptor[] fields = FieldDescriptor.fromFields(klass.getDeclaredFields()); 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.append("declared fields {\n"); 29 | 30 | for (FieldDescriptor field : fields) { 31 | sb.append(Utils.indent(1)); 32 | sb.append(field.toString()); 33 | sb.append("\n"); 34 | } 35 | 36 | sb.append("}"); 37 | 38 | interpreter.println(sb.toString()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/get.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | import bsh.CallStack; 6 | import bsh.Interpreter; 7 | import jwf.debugport.annotations.Command; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_ACCESS) 13 | public class get { 14 | @Command.Help("Get the value of a field, regardless of access modifiers, on the provided object.") 15 | public static Object invoke( 16 | Interpreter interpreter, 17 | CallStack callstack, 18 | @Command.ParamName("obj") Object object, 19 | @Command.ParamName("fieldName") String param) throws NoSuchFieldException, IllegalAccessException { 20 | Field field; 21 | try { 22 | field = object.getClass().getDeclaredField(param); 23 | } catch (NoSuchFieldException e) { 24 | // let's try a regular field.. 25 | field = object.getClass().getField(param); 26 | } 27 | field.setAccessible(true); 28 | return field.get(object); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/help.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import android.util.Log; 4 | 5 | import java.util.ArrayList; 6 | import java.util.Collections; 7 | 8 | import bsh.CallStack; 9 | import bsh.Interpreter; 10 | import jwf.debugport.annotations.Command; 11 | import jwf.debugport.internal.CommandHelpInfo; 12 | import jwf.debugport.internal.Utils; 13 | 14 | /** 15 | * 16 | */ 17 | @Command 18 | public class help { 19 | private static final ArrayList sCommandHelp = new ArrayList<>(); 20 | private static final int MAX_WIDTH = 100; 21 | private static final int NAME_START_COL = 6; 22 | private static final int HELP_START_COL = 10; 23 | private static final int GROUP_START_COL = 2; 24 | 25 | @Command.Help("Show this help message.") 26 | public static void invoke(Interpreter interpreter, CallStack callStack) { 27 | try { 28 | StringBuilder builder = new StringBuilder(); 29 | builder.append("Available Commands:"); 30 | String lastCommandGroup = "__not_a_group__"; 31 | for (CommandHelpInfo info : sCommandHelp) { 32 | builder.append("\n"); 33 | String commandGroup = info.getCommandGroup(); 34 | if (!commandGroup.equalsIgnoreCase(lastCommandGroup)) { 35 | if (!lastCommandGroup.equals("__not_a_group__")) { 36 | builder.append("\n"); 37 | } 38 | builder.append(Utils.spaces(GROUP_START_COL)) 39 | .append(commandGroup) 40 | .append(":") 41 | .append("\n"); 42 | lastCommandGroup = commandGroup; 43 | } 44 | info.appendHelpInfoForMethods(builder, NAME_START_COL, HELP_START_COL, MAX_WIDTH); 45 | } 46 | builder.append("\n"); 47 | 48 | interpreter.println(builder.toString()); 49 | } catch (Exception e) { 50 | Log.e("OOPS", "hmm", e); 51 | } 52 | } 53 | 54 | public static void registerCommand(Class command) { 55 | if (!command.isAnnotationPresent(Command.class)) { 56 | return; 57 | } 58 | sCommandHelp.add(new CommandHelpInfo(command)); 59 | Collections.sort(sCommandHelp); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/methods.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import bsh.CallStack; 4 | import bsh.Interpreter; 5 | import jwf.debugport.annotations.Command; 6 | import jwf.debugport.internal.Utils; 7 | import jwf.debugport.internal.debug.commands.descriptors.MethodDescriptor; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_METHOD_INSPECTION) 13 | public class methods { 14 | @Command.Help("Get the available methods for the provided object.") 15 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("obj") Object obj) { 16 | invoke(interpreter, callStack, obj.getClass()); 17 | } 18 | 19 | @Command.Help("Get the available methods for the provided class.") 20 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("class") Class klass) { 21 | if (klass == null) { 22 | interpreter.println("value is null"); 23 | return; 24 | } 25 | MethodDescriptor[] methods = MethodDescriptor.fromMethods(klass.getMethods()); 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.append("available methods: {\n"); 29 | for (MethodDescriptor method : methods) { 30 | sb.append(Utils.indent(1)); 31 | sb.append(method.toString()); 32 | sb.append("\n"); 33 | } 34 | sb.append("}"); 35 | 36 | interpreter.println(sb.toString()); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/methodsLocal.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import bsh.CallStack; 4 | import bsh.Interpreter; 5 | import jwf.debugport.annotations.Command; 6 | import jwf.debugport.internal.Utils; 7 | import jwf.debugport.internal.debug.commands.descriptors.MethodDescriptor; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_METHOD_INSPECTION) 13 | public class methodsLocal { 14 | @Command.Help("Show all of the locally-declared methods for the provided object.") 15 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("obj") Object obj) { 16 | invoke(interpreter, callStack, obj.getClass()); 17 | } 18 | 19 | @Command.Help("Show all of the locally-declared methods for the provided class.") 20 | public static void invoke(Interpreter interpreter, CallStack callStack, @Command.ParamName("class") Class klass) { 21 | if (klass == null) { 22 | interpreter.println("value is null"); 23 | return; 24 | } 25 | MethodDescriptor[] methods = MethodDescriptor.fromMethods(klass.getDeclaredMethods()); 26 | StringBuilder sb = new StringBuilder(); 27 | 28 | sb.append("declared methods: {\n"); 29 | for (MethodDescriptor method : methods) { 30 | sb.append(Utils.indent(1)); 31 | sb.append(method.toString()); 32 | sb.append("\n"); 33 | } 34 | sb.append("}"); 35 | 36 | interpreter.println(sb.toString()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/set.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import java.lang.reflect.Field; 4 | 5 | import bsh.CallStack; 6 | import bsh.Interpreter; 7 | import jwf.debugport.annotations.Command; 8 | 9 | /** 10 | * 11 | */ 12 | @Command(group = Command.GROUP_ACCESS) 13 | public class set { 14 | @Command.Help("Set the value of a field on the provided object to the given value, regardless of access modifiers.") 15 | public static Object invoke( 16 | Interpreter interpreter, 17 | CallStack callStack, 18 | @Command.ParamName("obj") Object object, 19 | @Command.ParamName("fieldName") String param, 20 | @Command.ParamName("value") Object value) throws NoSuchFieldException, IllegalAccessException { 21 | Field field; 22 | try { 23 | field = object.getClass().getDeclaredField(param); 24 | } catch (NoSuchFieldException e) { 25 | // try a field on the inherited classes 26 | field = object.getClass().getField(param); 27 | } 28 | field.setAccessible(true); 29 | field.set(object, value); 30 | return value; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/debug/commands/source.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.debug.commands; 2 | 3 | import android.app.Application; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStreamReader; 7 | import java.io.Reader; 8 | 9 | import bsh.CallStack; 10 | import bsh.EvalError; 11 | import bsh.Interpreter; 12 | import jwf.debugport.annotations.Command; 13 | 14 | /** 15 | * 16 | */ 17 | @Command 18 | public class source { 19 | @Command.Help("Load and run a Beanshell script within your app's assets folder.") 20 | public static Object invoke( 21 | Interpreter interpreter, 22 | CallStack callStack, 23 | @Command.ParamName("scriptPath") String assetScriptPath) throws EvalError, IOException { 24 | Application app = (Application) interpreter.get("app"); 25 | Reader scriptIn = new InputStreamReader(app.getAssets().open(assetScriptPath)); 26 | Object result = interpreter.eval(scriptIn); 27 | scriptIn.close(); 28 | return result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/SQLiteClientConnection.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite; 2 | 3 | import android.content.Context; 4 | import android.database.Cursor; 5 | import android.database.sqlite.SQLiteDatabase; 6 | import android.database.sqlite.SQLiteException; 7 | 8 | import java.io.PrintWriter; 9 | import java.net.Socket; 10 | import java.util.Scanner; 11 | 12 | import jwf.debugport.internal.ClientConnection; 13 | import jwf.debugport.internal.TelnetServer; 14 | import jwf.debugport.internal.Utils; 15 | import jwf.debugport.internal.sqlite.commands.Commands; 16 | import jwf.debugport.internal.sqlite.commands.SQLiteCommand; 17 | 18 | /** 19 | * ClientConnection implementation for an SQLite REPL with MySQL-style params. 20 | */ 21 | public class SQLiteClientConnection extends ClientConnection { 22 | private SQLiteOpenHelper mCurrentDb; 23 | 24 | public SQLiteClientConnection(Context context, Socket client, TelnetServer parent, String[] startupCommands) { 25 | super(context, client, parent, startupCommands); 26 | } 27 | 28 | @Override 29 | public void closeConnection() { 30 | if (mCurrentDb != null) { 31 | mCurrentDb.close(); 32 | } 33 | } 34 | 35 | @Override 36 | public void run() { 37 | try { 38 | Socket s = getSocket(); 39 | Scanner in = new Scanner(s.getInputStream()); 40 | in.useDelimiter(";"); 41 | PrintWriter out = new PrintWriter(s.getOutputStream(), true); 42 | 43 | out.println("SQLite Database REPL"); 44 | boolean wasExitCommand = false; 45 | do { 46 | out.println(); 47 | out.print("sqlite> "); 48 | out.flush(); 49 | 50 | String commandCandidate = in.next().trim(); 51 | SQLiteCommand command = Commands.getInstance().getCommand(commandCandidate); 52 | if (command != null) { 53 | command.execute(this, out, commandCandidate); 54 | wasExitCommand = command.isExitCommand(); 55 | } else { 56 | if (mCurrentDb != null) { 57 | SQLiteDatabase db = mCurrentDb.open(); 58 | long start = System.nanoTime(); 59 | try { 60 | Cursor c = db.rawQuery(commandCandidate, null); 61 | int rows = c.getCount(); 62 | long elapsed = System.nanoTime() - start; 63 | Utils.printTable(out, c); 64 | 65 | if (rows > 0) { 66 | out.println(rows + " rows in set (" + Utils.nanosToMillis(elapsed) + ")"); 67 | } else { 68 | out.println("Query executed in " + Utils.nanosToMillis(elapsed) + "."); 69 | } 70 | } catch (SQLiteException sqle) { 71 | out.println(sqle.getMessage()); 72 | } 73 | } else { 74 | printNoDBSelected(out); 75 | } 76 | } 77 | } while (!wasExitCommand); 78 | } catch (Exception e) { 79 | logError("Error:", e); 80 | } finally { 81 | close(); 82 | } 83 | } 84 | 85 | public void printNoDBSelected(PrintWriter out) { 86 | out.println("No database currently selected. Call `use [table name];` to select one, or `show databases;` to list available databases."); 87 | } 88 | 89 | public void setCurrentDatabase(String dbName) { 90 | if (mCurrentDb != null) { 91 | mCurrentDb.close(); 92 | } 93 | mCurrentDb = new SQLiteOpenHelper(this, dbName); 94 | mCurrentDb.open(); 95 | } 96 | 97 | public SQLiteDatabase getCurrentDatabase() { 98 | if (mCurrentDb == null) { 99 | return null; 100 | } 101 | return mCurrentDb.open(); 102 | } 103 | 104 | public boolean databaseExists(String dbName) { 105 | String[] databases = getApp().databaseList(); 106 | boolean exists = false; 107 | for (String existingDb : databases) { 108 | if (existingDb.equals(dbName)) { 109 | return true; 110 | } 111 | } 112 | return false; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/SQLiteOpenHelper.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite; 2 | 3 | import android.app.Application; 4 | import android.content.Context; 5 | import android.database.sqlite.SQLiteDatabase; 6 | 7 | /** 8 | * Created by jason on 5/9/16. 9 | */ 10 | public class SQLiteOpenHelper { 11 | private final Application mApp; 12 | private final String mName; 13 | private boolean mCreated; 14 | private SQLiteDatabase mDatabase; 15 | 16 | public SQLiteOpenHelper(SQLiteClientConnection connection, String name) { 17 | mApp = connection.getApp(); 18 | mName = name; 19 | } 20 | 21 | public SQLiteDatabase open() { 22 | if (mDatabase == null) { 23 | mDatabase = mApp.openOrCreateDatabase(mName, Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null); 24 | } 25 | return mDatabase; 26 | } 27 | 28 | public void close() { 29 | if (mDatabase != null && mDatabase.isOpen()) { 30 | mDatabase.close(); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/SQLiteTelnetServer.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite; 2 | 3 | import android.content.Context; 4 | 5 | import java.net.Socket; 6 | import java.net.UnknownHostException; 7 | 8 | import jwf.debugport.Params; 9 | import jwf.debugport.internal.TelnetServer; 10 | 11 | /** 12 | * Implementation of {@link TelnetServer} to support a REPL to access SQLite Databases. 13 | */ 14 | public class SQLiteTelnetServer extends TelnetServer { 15 | public SQLiteTelnetServer(Context app, Params params) throws UnknownHostException { 16 | super(app, params, params.getSQLitePort()); 17 | } 18 | 19 | @Override 20 | public SQLiteClientConnection getClientConnection(Context c, Socket socket, TelnetServer server, String[] startupCommands) { 21 | return new SQLiteClientConnection(c, socket, server, startupCommands); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/Commands.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | /** 4 | * 5 | */ 6 | public final class Commands { 7 | private static Commands sInstance; 8 | private final SQLiteCommand[] mAvailableCommands; 9 | 10 | private Commands() { 11 | mAvailableCommands = new SQLiteCommand[] { 12 | new ExitCommand(), 13 | new ShowDatabasesCommand(), 14 | new CreateDatabaseCommand(), 15 | new DropDatabaseCommand(), 16 | new UseCommand(), 17 | new ShowTablesCommand(), 18 | new ShowCreateTableCommand(), 19 | new HelpCommand(), 20 | }; 21 | } 22 | 23 | public final SQLiteCommand[] getCommands() { 24 | return mAvailableCommands; 25 | } 26 | 27 | public SQLiteCommand getCommand(String input) { 28 | for (SQLiteCommand cmd : mAvailableCommands) { 29 | if (cmd.isCommandString(input)) { 30 | return cmd; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | public static final Commands getInstance() { 37 | if (sInstance == null) { 38 | sInstance = new Commands(); 39 | } 40 | return sInstance; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/CreateDatabaseCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import android.app.Application; 4 | 5 | import java.io.PrintWriter; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import jwf.debugport.annotations.Command; 10 | import jwf.debugport.internal.Utils; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | import jwf.debugport.internal.sqlite.SQLiteOpenHelper; 13 | 14 | /** 15 | * Created by jason on 5/9/16. 16 | */ 17 | @Command.Help( 18 | format = "CREATE DATABASE [database name];", 19 | value = "Create a new database called [database name].", 20 | group = Command.GROUP_SQL_DATABASES 21 | ) 22 | public class CreateDatabaseCommand extends SQLiteCommand { 23 | private static final Pattern command = Pattern.compile("\\s*create\\s+database\\s+(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 24 | 25 | @Override 26 | public boolean isCommandString(String candidate) { 27 | return command.matcher(candidate).matches(); 28 | } 29 | 30 | @Override 31 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 32 | Matcher commandMatcher = command.matcher(commandString); 33 | if (!commandMatcher.matches()) { 34 | return; 35 | } 36 | String dbName = commandMatcher.group(1); 37 | 38 | if (!isValidDatabaseName(dbName)) { 39 | out.println("Invalid database name: "+dbName); 40 | out.flush(); 41 | } else { 42 | Application app = connection.getApp(); 43 | 44 | if (connection.databaseExists(dbName)) { 45 | out.println("Database `" + dbName + "` already exists"); 46 | return; 47 | } 48 | 49 | long start = System.nanoTime(); 50 | SQLiteOpenHelper helper = new SQLiteOpenHelper(connection, dbName); 51 | helper.open(); 52 | long elapsed = System.nanoTime() - start; 53 | helper.close(); 54 | out.println("Database `" + dbName + "` created in " + Utils.nanosToMillis(elapsed)); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/DropDatabaseCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import android.app.Application; 4 | 5 | import java.io.PrintWriter; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | import jwf.debugport.annotations.Command; 10 | import jwf.debugport.internal.Utils; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | 13 | /** 14 | * Created by jason on 5/9/16. 15 | */ 16 | @Command.Help( 17 | format = "DROP DATABASE [database name];", 18 | value = "Drop the database named [database name] from the app's collection of databases.", 19 | group = Command.GROUP_SQL_DATABASES 20 | ) 21 | public class DropDatabaseCommand extends SQLiteCommand { 22 | private static final Pattern command = Pattern.compile("\\s*drop\\s+database\\s+(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 23 | 24 | @Override 25 | public boolean isCommandString(String candidate) { 26 | return command.matcher(candidate).matches(); 27 | } 28 | 29 | @Override 30 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 31 | Matcher commandMatcher = command.matcher(commandString); 32 | if (!commandMatcher.matches()) { 33 | return; 34 | } 35 | String dbName = commandMatcher.group(1); 36 | 37 | if (!isValidDatabaseName(dbName)) { 38 | out.println("Invalid database name: "+dbName); 39 | out.flush(); 40 | } else { 41 | Application app = connection.getApp(); 42 | 43 | String[] databases = app.databaseList(); 44 | boolean exists = false; 45 | if (!connection.databaseExists(dbName)) { 46 | out.println("Database with name `"+dbName+"` does not exist"); 47 | return; 48 | } 49 | 50 | long start = System.nanoTime(); 51 | app.deleteDatabase(dbName); 52 | long elapsed = System.nanoTime() - start; 53 | out.println("Database `" + dbName + "` dropped in " + Utils.nanosToMillis(elapsed)); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/ExitCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.regex.Pattern; 5 | 6 | import jwf.debugport.annotations.Command; 7 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 8 | 9 | /** 10 | * Created by jason on 5/9/16. 11 | */ 12 | @Command.Help( 13 | value = "Exit this interpreter.", 14 | format = "exit; or quit;" 15 | ) 16 | public class ExitCommand extends SQLiteCommand { 17 | private static final Pattern command = Pattern.compile("^\\s*exit|quit\\s*$", Pattern.CASE_INSENSITIVE); 18 | 19 | @Override 20 | public boolean isCommandString(String candidate) { 21 | return command.matcher(candidate).matches(); 22 | } 23 | 24 | @Override 25 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 26 | out.println("Thanks for using Android DebugPort!"); 27 | out.flush(); 28 | } 29 | 30 | @Override 31 | public boolean isExitCommand() { 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/HelpCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.regex.Pattern; 7 | 8 | import jwf.debugport.annotations.Command; 9 | import jwf.debugport.internal.CommandHelpInfo; 10 | import jwf.debugport.internal.Utils; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | 13 | /** 14 | * Created by jason on 5/15/16. 15 | */ 16 | @Command.Help( 17 | value = "Show this help message.", 18 | format = "help;" 19 | ) 20 | public class HelpCommand extends SQLiteCommand { 21 | private static final Pattern command = Pattern.compile("^help$", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 22 | private static final int MAX_WIDTH = 100; 23 | private static final int PREAMBLE_START_COL = 2; 24 | private static final int NAME_START_COL = 8; 25 | private static final int HELP_START_COL = 12; 26 | private static final int GROUP_START_COL = 4; 27 | 28 | @Override 29 | public boolean isCommandString(String candidate) { 30 | return command.matcher(candidate).matches(); 31 | } 32 | 33 | @Override 34 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 35 | SQLiteCommand[] commands = Commands.getInstance().getCommands(); 36 | ArrayList helpInfo = new ArrayList<>(); 37 | for (SQLiteCommand cmd : commands) { 38 | if (cmd.getClass().isAnnotationPresent(Command.Help.class)) { 39 | helpInfo.add(new CommandHelpInfo(cmd.getClass())); 40 | } 41 | } 42 | Collections.sort(helpInfo); 43 | 44 | StringBuilder builder = new StringBuilder(); 45 | builder.append("Help:\n"); 46 | CommandHelpInfo.appendHelpText( 47 | builder, 48 | "As you'd expect, you can execute any valid SQLite statements against the database " + 49 | "to which you're currently connected (see: `USE [database name];` below).", 50 | PREAMBLE_START_COL, 51 | MAX_WIDTH); 52 | builder.append("\n\n"); 53 | CommandHelpInfo.appendHelpText( 54 | builder, 55 | "In addition to regular SQLite commands, Android DebugPort provides additional " + 56 | "functionality via several additional commands.", 57 | PREAMBLE_START_COL, 58 | MAX_WIDTH); 59 | builder.append("\n\n"); 60 | CommandHelpInfo.appendHelpText( 61 | builder, 62 | "Available non-SQLite commands (case insensitive):", 63 | PREAMBLE_START_COL, 64 | MAX_WIDTH); 65 | String lastCommandGroup = "__not_a_group__"; 66 | for (CommandHelpInfo info : helpInfo) { 67 | builder.append("\n"); 68 | String commandGroup = info.getCommandGroup(); 69 | if (!commandGroup.equalsIgnoreCase(lastCommandGroup)) { 70 | if (!lastCommandGroup.equals("__not_a_group__")) { 71 | builder.append("\n"); 72 | } 73 | builder.append(Utils.spaces(GROUP_START_COL)) 74 | .append(commandGroup) 75 | .append(":") 76 | .append("\n"); 77 | lastCommandGroup = commandGroup; 78 | } 79 | info.appendHelpInfoForClass(builder, NAME_START_COL, HELP_START_COL, MAX_WIDTH); 80 | } 81 | builder.append("\n"); 82 | 83 | out.print(builder.toString()); 84 | out.flush(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/SQLiteCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.regex.Pattern; 5 | 6 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 7 | 8 | /** 9 | * Abstract command.. 10 | */ 11 | public abstract class SQLiteCommand { 12 | protected static final Pattern DATABASE_NAME = Pattern.compile("[a-zA-Z][_a-zA-Z0-9]*"); 13 | protected static final Pattern TABLE_NAME = Pattern.compile("[a-zA-Z][_a-zA-Z0-9]*"); 14 | 15 | /** 16 | * Whether or not the given string is a match for this command. 17 | */ 18 | public abstract boolean isCommandString(String candidate); 19 | 20 | /** 21 | * Execute the command. 22 | */ 23 | public abstract void execute(SQLiteClientConnection connection, PrintWriter out, String commandString); 24 | 25 | public boolean isExitCommand() { 26 | return false; 27 | } 28 | 29 | protected final boolean isValidDatabaseName(String dbName) { 30 | return DATABASE_NAME.matcher(dbName).matches(); 31 | } 32 | 33 | protected final boolean isValidTableName(String tableName) { 34 | return TABLE_NAME.matcher(tableName).matches(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/ShowCreateTableCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import android.database.Cursor; 4 | import android.database.sqlite.SQLiteDatabase; 5 | 6 | import java.io.PrintWriter; 7 | import java.util.regex.Matcher; 8 | import java.util.regex.Pattern; 9 | 10 | import jwf.debugport.annotations.Command; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | 13 | /** 14 | * Print the CREATE TABLE syntax for a table. 15 | */ 16 | @Command.Help( 17 | format = "SHOW CREATE TABLE [table name];", 18 | value = "Show the CREATE TABLE command used to create [table name].", 19 | group = Command.GROUP_SQL_INSPECTION 20 | ) 21 | public class ShowCreateTableCommand extends SQLiteCommand { 22 | private static final Pattern command = Pattern.compile("(show\\s+create\\s+table|schema)\\s+(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 23 | 24 | @Override 25 | public boolean isCommandString(String candidate) { 26 | return command.matcher(candidate).matches(); 27 | } 28 | 29 | @Override 30 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 31 | Matcher m = command.matcher(commandString); 32 | if (!m.matches()) { 33 | return; 34 | } 35 | 36 | String tableName = m.group(2); 37 | if (!isValidTableName(tableName)) { 38 | out.println("Invalid table name `"+tableName+"`"); 39 | } else { 40 | SQLiteDatabase db = connection.getCurrentDatabase(); 41 | Cursor c = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[]{tableName}); 42 | if (c.getCount() == 0) { 43 | out.println("Table `"+tableName+"` does not exist."); 44 | } else { 45 | c.moveToNext(); 46 | out.print(c.getString(0)); 47 | out.println(":"); 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/ShowDatabasesCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import android.app.Application; 4 | 5 | import java.io.PrintWriter; 6 | import java.util.Arrays; 7 | import java.util.regex.Pattern; 8 | 9 | import jwf.debugport.annotations.Command; 10 | import jwf.debugport.internal.Utils; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | 13 | /** 14 | * Created by jason on 5/9/16. 15 | */ 16 | @Command.Help( 17 | format = "SHOW DATABASES;", 18 | value = "Show all available databases for the app, including temporary databases.", 19 | group = Command.GROUP_SQL_INSPECTION 20 | ) 21 | public class ShowDatabasesCommand extends SQLiteCommand { 22 | private static final Pattern command = Pattern.compile("show\\s+databases", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 23 | @Override 24 | public boolean isCommandString(String candidate) { 25 | return command.matcher(candidate).matches(); 26 | } 27 | 28 | @Override 29 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 30 | Application app = connection.getApp(); 31 | String[] databaseList = app.databaseList(); 32 | Arrays.sort(databaseList); 33 | Object[][] data = new Object[databaseList.length][1]; 34 | for (int i = 0; i < databaseList.length; i++) { 35 | data[i][0] = databaseList[i]; 36 | } 37 | Utils.printTable(out, new String[] { "Database" }, data); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/ShowTablesCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import android.database.Cursor; 4 | import android.database.sqlite.SQLiteDatabase; 5 | 6 | import java.io.PrintWriter; 7 | import java.util.regex.Pattern; 8 | 9 | import jwf.debugport.annotations.Command; 10 | import jwf.debugport.internal.Utils; 11 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 12 | 13 | /** 14 | * Created by jason on 5/9/16. 15 | */ 16 | @Command.Help( 17 | format = "SHOW TABLES;", 18 | value = "Show all of the tables defined for the database to which you are currently connected.", 19 | group = Command.GROUP_SQL_INSPECTION 20 | ) 21 | public class ShowTablesCommand extends SQLiteCommand { 22 | public static final Pattern command = Pattern.compile("show\\s+tables", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 23 | 24 | @Override 25 | public boolean isCommandString(String candidate) { 26 | return command.matcher(candidate).matches(); 27 | } 28 | 29 | @Override 30 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 31 | SQLiteDatabase db = connection.getCurrentDatabase(); 32 | if (db == null) { 33 | connection.printNoDBSelected(out); 34 | return; 35 | } 36 | 37 | Cursor c = db.rawQuery("SELECT name AS 'Table' FROM sqlite_master WHERE type = 'table' ORDER BY name", null); 38 | Utils.printTable(out, c); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /lib/src/main/java/jwf/debugport/internal/sqlite/commands/UseCommand.java: -------------------------------------------------------------------------------- 1 | package jwf.debugport.internal.sqlite.commands; 2 | 3 | import java.io.PrintWriter; 4 | import java.util.regex.Matcher; 5 | import java.util.regex.Pattern; 6 | 7 | import jwf.debugport.annotations.Command; 8 | import jwf.debugport.internal.sqlite.SQLiteClientConnection; 9 | 10 | /** 11 | * 12 | */ 13 | @Command.Help( 14 | format = "USE [database name];", 15 | value = "Connect to the database called [database name]. All SQL commands will be executed against this database until USE is called again.", 16 | group = Command.GROUP_SQL_DATABASES 17 | ) 18 | public class UseCommand extends SQLiteCommand { 19 | private static final Pattern command = Pattern.compile("\\s*use\\s+(\\w+)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE); 20 | 21 | @Override 22 | public boolean isCommandString(String candidate) { 23 | return command.matcher(candidate).matches(); 24 | } 25 | 26 | @Override 27 | public void execute(SQLiteClientConnection connection, PrintWriter out, String commandString) { 28 | Matcher commandMatcher = command.matcher(commandString); 29 | if (!commandMatcher.matches()) { 30 | return; 31 | } 32 | String dbName = commandMatcher.group(1); 33 | 34 | if (!isValidDatabaseName(dbName)) { 35 | out.println("Invalid database name: "+dbName); 36 | out.flush(); 37 | } else { 38 | if (!connection.databaseExists(dbName)) { 39 | out.println("Database `"+dbName+"` does not exist"); 40 | return; 41 | } 42 | 43 | connection.setCurrentDatabase(dbName); 44 | out.println("Using database `"+dbName+"`"); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/src/main/res/drawable/debugport_ic_notification.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /lib/src/main/res/values/ids.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /lib/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Android DebugPort 3 | Android DebugPort: %s 4 | Debug Running at %1$s:%2$d\nSQLite Running at %1$s:%3$d 5 | Stopped 6 | Debug: %1$s:%2$d, SQLite: %1$s:%3$d 7 | Stopped 8 | Stop 9 | Start 10 | Kill 11 | Android DebugPort 12 | 13 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':lib', ':lib-noop' 2 | --------------------------------------------------------------------------------