├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── images ├── web_ui_1.jpg └── web_ui_1.png ├── index.html ├── remotelogcat ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zzzmode │ │ └── android │ │ └── remotelogcat │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── zzzmode │ │ └── android │ │ ├── internel │ │ └── ThreadUtil.java │ │ ├── remotelogcat │ │ ├── AbsMontitor.java │ │ ├── IMontitor.java │ │ ├── LogcatRunner.java │ │ └── MemoryMontitor.java │ │ └── server │ │ ├── Base64.java │ │ ├── EncodeUtils.java │ │ ├── HttpResponse.java │ │ ├── LineInputStream.java │ │ ├── NetworkUtils.java │ │ ├── WebSocket.java │ │ ├── WebSocketImpl.java │ │ └── WebSocketServer.java │ └── test │ └── java │ └── com │ └── zzzmode │ └── android │ └── remotelogcat │ └── ExampleUnitTest.java ├── remotelogcatsample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── zzzmode │ │ └── android │ │ └── remotelogcatsample │ │ └── ApplicationTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── zzzmode │ │ │ └── android │ │ │ └── remotelogcatsample │ │ │ └── MainActivity.java │ └── res │ │ ├── layout │ │ └── activity_main.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-v21 │ │ └── styles.xml │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── zzzmode │ └── android │ └── remotelogcatsample │ └── ExampleUnitTest.java └── 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 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RemoteLogcatViewer 2 | 在浏览器上远程查看logcat日志。 3 | 4 | ## 用法 5 | ```gradle 6 | compile project(':remotelogcat') 7 | ``` 8 | 9 | ```java 10 | //start 11 | LogcatRunner.getInstance().config(LogcatRunner.LogConfig.builder().write2File(true)).start(); 12 | ... 13 | //stop 14 | LogcatRunner.getInstance().stop(); 15 | ``` 16 | 17 | 然后在浏览器中打开index.html 输入对应局域网ip和端口`ws://ip:port/logcat` 即可(注:logcat别名可以修改)。 18 | > 因为一些安全原因,chrome禁止了部分不安全的请求地址`ws`,可以`允许加载不安全脚本`继续使用或者下载`index.html` 文件本地打开也可以。 19 | 20 | 21 | 如果不希望修改现有项目,可以新建一个其他的项目依赖本库,然后通过配置相同的 `android:sharedUserId=""` 和签名相同, 22 | 可以在新app运行时中读取所有sharedUserId相同的 log。 23 | 24 | ## 实现原理 25 | 原理非常简单,在内部使用`Runtime.getRuntime().exec("logcat");` 执行命令去获取logcat输出流,然后逐行读取后通过websocket输出要远端,为了尽可能节省性能,只会维护一个客户端输出。 26 | 注意只能输出自己包下的log日志,当然相同sharedUserId和签名的也可以,多进程情况下建议在常驻后台的Service中启动本监听。 27 | 28 | ## 作用 29 | 某些Android设备没有调试接口,比如电视或者各种盒子终端,没法连接usb调试当然也不能查看logcat日志了,这个项目是在浏览器上远程显示和保存logcat输出,帮助调试开发使用。 30 | 31 | ## 功能 32 | 目前可以完整的查看、过滤、保存logcat信息。 33 | 支持日志文件写入、下载。 34 | 后期会加入shell支持。 35 | 36 | ## License 37 | Apache License 2.0 38 | -------------------------------------------------------------------------------- /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:1.5.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | 21 | task clean(type: Delete) { 22 | delete rootProject.buildDir 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## Project-wide Gradle settings. 2 | # 3 | # For more details on how to configure your build environment visit 4 | # http://www.gradle.org/docs/current/userguide/build_environment.html 5 | # 6 | # Specifies the JVM arguments used for the daemon process. 7 | # The setting is particularly useful for tweaking memory settings. 8 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 9 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 10 | # 11 | # When configured, Gradle will run in incubating parallel mode. 12 | # This option should only be used with decoupled projects. More details, visit 13 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 14 | # org.gradle.parallel=true 15 | #Sun Jan 24 14:52:24 CST 2016 16 | systemProp.http.proxyHost=127.0.0.1 17 | systemProp.http.nonProxyHosts=192.168.*,localhost 18 | systemProp.http.proxyPort=1080 19 | -------------------------------------------------------------------------------- /images/web_ui_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/images/web_ui_1.jpg -------------------------------------------------------------------------------- /images/web_ui_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/images/web_ui_1.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Android Remote Logcat 11 | 208 | 209 | 210 | 211 | 218 |
219 |
220 | Server Location 221 |
222 | 223 | 224 | 225 | 226 |
227 |
228 | 229 | CLOSED 230 |
231 |
232 |
233 | Request 234 |
235 | 236 | 237 | 245 | 246 |
247 |
248 | 249 | [Shortcut] Ctr + Enter 250 |
251 | 252 |
253 | 254 | 255 | 260 | 261 |
262 | 263 | 264 |
265 | 266 |
267 | 268 |
269 | 270 |
271 | 272 |
273 | 274 |
275 | 276 |
277 |

shell

278 | 279 |
280 | 281 |
282 | 283 | 681 | 682 | 683 | -------------------------------------------------------------------------------- /remotelogcat/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /remotelogcat/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion "27.0.3" 6 | 7 | defaultConfig { 8 | minSdkVersion 15 9 | targetSdkVersion 27 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | 24 | testImplementation 'junit:junit:4.12' 25 | } 26 | -------------------------------------------------------------------------------- /remotelogcat/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/zl/develop/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 | -------------------------------------------------------------------------------- /remotelogcat/src/androidTest/java/com/zzzmode/android/remotelogcat/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcat; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /remotelogcat/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/internel/ThreadUtil.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.internel; 2 | 3 | import java.util.concurrent.Executor; 4 | import java.util.concurrent.Executors; 5 | 6 | /** 7 | * Created by zl on 16/4/20. 8 | */ 9 | public class ThreadUtil { 10 | 11 | private static final Executor sExecutor= Executors.newCachedThreadPool(); 12 | 13 | public static Executor getExecutor(){ 14 | return sExecutor; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/remotelogcat/AbsMontitor.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcat; 2 | 3 | import android.content.Context; 4 | 5 | import org.json.JSONObject; 6 | 7 | /** 8 | * Created by zl on 16/4/20. 9 | */ 10 | public abstract class AbsMontitor implements IMontitor { 11 | 12 | public AbsMontitor(){ 13 | 14 | } 15 | 16 | protected OnNotifyObserver notifyObserver; 17 | protected Context context; 18 | 19 | public void attachServer(Context context,OnNotifyObserver observer){ 20 | this.context=context; 21 | this.notifyObserver=observer; 22 | } 23 | 24 | 25 | protected void notify(JSONObject jsonObject){ 26 | if(notifyObserver != null){ 27 | notifyObserver.onChange(jsonObject); 28 | } 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/remotelogcat/IMontitor.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcat; 2 | 3 | import android.content.Context; 4 | 5 | import org.json.JSONObject; 6 | 7 | /** 8 | * Created by zl on 16/4/20. 9 | */ 10 | public interface IMontitor extends Runnable{ 11 | 12 | int getInterval(); 13 | 14 | void start(); 15 | 16 | void stop(); 17 | 18 | interface OnNotifyObserver{ 19 | void onChange(JSONObject jsonObject); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/remotelogcat/LogcatRunner.java: -------------------------------------------------------------------------------- 1 | 2 | package com.zzzmode.android.remotelogcat; 3 | 4 | import android.content.Context; 5 | import android.os.Environment; 6 | import android.os.SystemClock; 7 | import android.util.Log; 8 | 9 | import com.zzzmode.android.server.WebSocket; 10 | import com.zzzmode.android.server.WebSocketServer; 11 | 12 | import org.json.JSONObject; 13 | 14 | import java.io.BufferedOutputStream; 15 | import java.io.BufferedReader; 16 | import java.io.File; 17 | import java.io.FileOutputStream; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.InputStreamReader; 21 | import java.text.SimpleDateFormat; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.Date; 25 | import java.util.HashSet; 26 | import java.util.List; 27 | import java.util.Locale; 28 | import java.util.Set; 29 | 30 | 31 | public class LogcatRunner implements IMontitor.OnNotifyObserver{ 32 | private static final String TAG = "LogcatRunner"; 33 | 34 | private static final int VERSION = 1; 35 | 36 | private static LogcatRunner sLogcatRunner; 37 | 38 | private ShellProcessThread mLogcatThread; 39 | 40 | private ShellProcessThread.ProcessOutputCallback mProcessOutputCallback; 41 | 42 | private WebSocketServer mWebSocketServer; 43 | 44 | private WebSocket mCurrWebSocket; 45 | 46 | private LogConfig mLogConfig; 47 | 48 | private Context mContext; 49 | private Set mMontitors=new HashSet<>(); 50 | private Set> mMontitorCls=new HashSet<>(); 51 | 52 | public static LogcatRunner getInstance() { 53 | if (sLogcatRunner == null) { 54 | synchronized (LogcatRunner.class) { 55 | if (sLogcatRunner == null) { 56 | sLogcatRunner = new LogcatRunner(); 57 | } 58 | } 59 | } 60 | return sLogcatRunner; 61 | } 62 | 63 | 64 | private LogcatRunner() { 65 | 66 | } 67 | 68 | 69 | private void init() throws IOException { 70 | if (mLogConfig == null) { 71 | mLogConfig = LogConfig.builder(); 72 | } 73 | 74 | mWebSocketServer = new WebSocketServer(mLogConfig.port, mLogConfig.wsPrefix); 75 | mWebSocketServer.setWsCanRead(mLogConfig.wsCanReceiveMsg); 76 | 77 | mWebSocketServer.setWebSocketServerCallback(new WebSocketServer.WebSocketServerCallback() { 78 | @Override 79 | public void onConnected(WebSocket webSocket) { 80 | try { 81 | if (mCurrWebSocket != null) { 82 | mCurrWebSocket.close(); 83 | } 84 | } catch (IOException e) { 85 | e.printStackTrace(); 86 | } 87 | 88 | mCurrWebSocket = webSocket; 89 | 90 | if (mCurrWebSocket != null) { 91 | 92 | mCurrWebSocket.setWebSocketCallback(new WebSocket.WebSocketCallback() { 93 | @Override 94 | public void onReceivedFrame(byte[] bytes) { 95 | Log.e(TAG, "onReceivedFrame --> " + new String(bytes)); 96 | } 97 | 98 | @Override 99 | public void onClosed() { 100 | Log.e(TAG, "onClosed --> " + mCurrWebSocket); 101 | mCurrWebSocket = null; 102 | } 103 | }); 104 | } 105 | } 106 | 107 | @Override 108 | public void onClosed() { 109 | 110 | } 111 | }); 112 | 113 | mProcessOutputCallback = new ShellProcessThread.ProcessOutputCallback() { 114 | 115 | @Override 116 | public void onReaderLine(String line) { 117 | sendText(line); 118 | } 119 | }; 120 | } 121 | @Override 122 | public void onChange(JSONObject jsonObject) { 123 | if(jsonObject != null){ 124 | sendText(jsonObject.toString()); 125 | } 126 | } 127 | 128 | public int getPort() { 129 | return mLogConfig.port; 130 | } 131 | 132 | 133 | public LogcatRunner config(LogConfig config) { 134 | mLogConfig = config; 135 | return this; 136 | } 137 | 138 | public LogcatRunner with(Context context){ 139 | if(context != null){ 140 | mContext=context.getApplicationContext(); 141 | //inner montitor 142 | mMontitorCls.add(MemoryMontitor.class); 143 | } 144 | return this; 145 | } 146 | 147 | 148 | public LogcatRunner addMotitor(Class... cls){ 149 | mMontitorCls.addAll(Arrays.asList(cls)); 150 | return this; 151 | } 152 | 153 | private void startMontitor(){ 154 | if(!mMontitorCls.isEmpty()){ 155 | for (Class cls:mMontitorCls){ 156 | try { 157 | AbsMontitor montitor= cls.newInstance(); 158 | montitor.attachServer(mContext,this); 159 | montitor.start(); 160 | mMontitors.add(montitor); 161 | } catch (Exception e) { 162 | e.printStackTrace(); 163 | } 164 | } 165 | } 166 | } 167 | 168 | private void stopMontitor(){ 169 | if(!mMontitors.isEmpty()){ 170 | for (IMontitor montitor:mMontitors){ 171 | montitor.stop(); 172 | } 173 | mMontitors.clear(); 174 | } 175 | } 176 | 177 | public void start() throws IOException { 178 | init(); 179 | mWebSocketServer.start(); 180 | startLogThread(); 181 | 182 | startMontitor(); 183 | } 184 | 185 | private void sendText(String string){ 186 | if (mCurrWebSocket != null && string != null) { 187 | try { 188 | mCurrWebSocket.send(string); 189 | } catch (IOException e) { 190 | e.printStackTrace(); 191 | } 192 | } 193 | } 194 | 195 | private void startLogThread() { 196 | try { 197 | if (mLogcatThread != null && mLogcatThread.isAlive()) { 198 | mLogcatThread.stopReader(); 199 | mLogcatThread.setOutputCallback(null); 200 | mLogcatThread.interrupt(); 201 | } 202 | } catch (Exception e) { 203 | e.printStackTrace(); 204 | } 205 | mLogcatThread = new ShellProcessThread(mLogConfig); 206 | mLogcatThread.setOutputCallback(mProcessOutputCallback); 207 | mLogcatThread.start(); 208 | 209 | } 210 | 211 | public void stop() { 212 | try { 213 | stopMontitor(); 214 | 215 | if (mWebSocketServer != null) { 216 | mWebSocketServer.stop(); 217 | } 218 | 219 | if (mLogcatThread != null && mLogcatThread.isAlive()) { 220 | mLogcatThread.stopReader(); 221 | mLogcatThread.setOutputCallback(null); 222 | mLogcatThread.interrupt(); 223 | } 224 | } catch (Exception e) { 225 | e.printStackTrace(); 226 | } 227 | } 228 | 229 | 230 | 231 | 232 | private static class ShellProcessThread extends Thread { 233 | 234 | private volatile boolean readerLogging = true; 235 | private ProcessOutputCallback mOutputCallback; 236 | private LogConfig logConfig; 237 | 238 | public ShellProcessThread(LogConfig logConfig) { 239 | this.logConfig = logConfig; 240 | } 241 | 242 | public void setOutputCallback(ProcessOutputCallback outputCallback) { 243 | mOutputCallback = outputCallback; 244 | } 245 | 246 | private File getLogFile() { 247 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()); 248 | String s = sdf.format(new Date()); 249 | File f = new File(logConfig.logFileDir + "/logcat-" + s + ".log"); 250 | if (!f.exists()) { 251 | try { 252 | f.getParentFile().mkdir(); 253 | int retry = 3; 254 | while (!f.createNewFile()) { 255 | if (retry < 0) { 256 | break; 257 | } 258 | SystemClock.sleep(100); 259 | retry--; 260 | } 261 | } catch (IOException e) { 262 | e.printStackTrace(); 263 | } 264 | } 265 | return f; 266 | } 267 | 268 | @Override 269 | public void run() { 270 | Process exec = null; 271 | InputStream inputStream = null; 272 | BufferedReader reader = null; 273 | 274 | FileOutputStream fos = null; 275 | BufferedOutputStream bos = null; 276 | try { 277 | 278 | exec = Runtime.getRuntime().exec(logConfig.logcatCMD); 279 | inputStream = exec.getInputStream(); 280 | 281 | try { 282 | if (logConfig.write2File) { 283 | File f = getLogFile(); 284 | Log.e(TAG, "--> write to file " + f); 285 | fos = new FileOutputStream(f, true); 286 | bos = new BufferedOutputStream(fos); 287 | StringBuilder sb = new StringBuilder("\n\n\n---------------\n\n"); 288 | sb.append(new Date().toLocaleString()); 289 | sb.append("\n\n-------------------\n\n\n"); 290 | bos.write(sb.toString().getBytes()); 291 | 292 | } 293 | } catch (IOException e) { 294 | e.printStackTrace(); 295 | } 296 | byte[] newLine = "\n".getBytes(); 297 | reader = new BufferedReader(new InputStreamReader(inputStream)); 298 | while (readerLogging) { 299 | String line = reader.readLine(); 300 | if (mOutputCallback != null) { 301 | mOutputCallback.onReaderLine(line); 302 | } 303 | 304 | try { 305 | if (logConfig.write2File && bos != null && line != null) { 306 | bos.write(line.getBytes()); 307 | bos.write(newLine); 308 | } 309 | } catch (IOException e) { 310 | e.printStackTrace(); 311 | } 312 | } 313 | 314 | if (bos != null) { 315 | bos.flush(); 316 | } 317 | } catch (Exception e) { 318 | e.printStackTrace(); 319 | } finally { 320 | try { 321 | if (fos != null && fos.getFD().valid()) { 322 | fos.getFD().sync(); 323 | } 324 | } catch (IOException e) { 325 | e.printStackTrace(); 326 | } 327 | try { 328 | if (bos != null) { 329 | bos.flush(); 330 | } 331 | } catch (IOException e) { 332 | e.printStackTrace(); 333 | } 334 | 335 | try { 336 | if (bos != null) { 337 | bos.close(); 338 | } 339 | } catch (IOException e) { 340 | e.printStackTrace(); 341 | } 342 | 343 | try { 344 | if (fos != null) { 345 | fos.close(); 346 | } 347 | } catch (IOException e) { 348 | e.printStackTrace(); 349 | } 350 | 351 | try { 352 | if (reader != null) { 353 | reader.close(); 354 | } 355 | } catch (IOException e) { 356 | e.printStackTrace(); 357 | } 358 | 359 | try { 360 | if (inputStream != null) { 361 | inputStream.close(); 362 | } 363 | } catch (IOException e) { 364 | e.printStackTrace(); 365 | } 366 | 367 | if (exec != null) { 368 | exec.destroy(); 369 | } 370 | } 371 | } 372 | 373 | public void stopReader() { 374 | readerLogging = false; 375 | } 376 | 377 | 378 | interface ProcessOutputCallback { 379 | void onReaderLine(String line); 380 | } 381 | } 382 | 383 | 384 | public static class LogConfig { 385 | 386 | private int port = 11229; 387 | private boolean write2File = false; 388 | private String logFileDir = Environment.getExternalStorageDirectory() + "/log"; 389 | private String wsPrefix = "/logcat"; 390 | private String logcatCMD = "logcat -v time"; 391 | private boolean wsCanReceiveMsg=false; 392 | 393 | public static LogConfig builder() { 394 | return new LogConfig(); 395 | } 396 | 397 | public LogConfig port(int port) { 398 | this.port = port; 399 | return this; 400 | } 401 | 402 | public LogConfig write2File(boolean write2File) { 403 | this.write2File = write2File; 404 | return this; 405 | } 406 | 407 | public LogConfig setLogFileDir(String logFileDir) { 408 | this.logFileDir = logFileDir; 409 | return this; 410 | } 411 | 412 | public LogConfig setWebsocketPrefix(String prefix) { 413 | this.wsPrefix = prefix; 414 | return this; 415 | } 416 | 417 | public LogConfig setWsCanReceiveMsg(boolean b){ 418 | this.wsCanReceiveMsg=b; 419 | return this; 420 | } 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/remotelogcat/MemoryMontitor.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcat; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | import android.os.Debug; 6 | import android.os.Process; 7 | 8 | import org.json.JSONObject; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.Executors; 12 | import java.util.concurrent.ScheduledExecutorService; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | /** 16 | * Created by zl on 16/4/20. 17 | */ 18 | class MemoryMontitor extends AbsMontitor{ 19 | 20 | private ScheduledExecutorService mExecutorService; 21 | private ActivityManager am; 22 | 23 | public MemoryMontitor(){ 24 | } 25 | 26 | @Override 27 | public void attachServer(Context context, OnNotifyObserver observer) { 28 | super.attachServer(context, observer); 29 | 30 | if(context != null) { 31 | am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 32 | mExecutorService = Executors.newScheduledThreadPool(1); 33 | } 34 | } 35 | 36 | @Override 37 | public int getInterval() { 38 | return 2; 39 | } 40 | 41 | @Override 42 | public void start() { 43 | mExecutorService.scheduleWithFixedDelay(this,0,getInterval(), TimeUnit.SECONDS); 44 | } 45 | 46 | @Override 47 | public void stop() { 48 | try { 49 | mExecutorService.shutdownNow(); 50 | } catch (Exception e) { 51 | e.printStackTrace(); 52 | } 53 | } 54 | 55 | @Override 56 | public void run() { 57 | List runningAppProcesses = am.getRunningAppProcesses(); 58 | if(runningAppProcesses != null){ 59 | final int myuid=Process.myUid(); 60 | int[] pids = new int[1]; 61 | for (ActivityManager.RunningAppProcessInfo info:runningAppProcesses){ 62 | try { 63 | if(info.uid == myuid){ 64 | pids[0]=info.pid; 65 | Debug.MemoryInfo[] memoryInfo = am.getProcessMemoryInfo(pids); 66 | preData(info.processName,memoryInfo[0].dalvikPrivateDirty); 67 | } 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | } 73 | } 74 | 75 | private void preData(String processName,int size){ 76 | try { 77 | JSONObject jsonObject =new JSONObject(); 78 | jsonObject.put("key","memory"); 79 | 80 | JSONObject data=new JSONObject(); 81 | data.put("process_name",processName); 82 | data.put("memory_size",size); 83 | 84 | jsonObject.put("data",data); 85 | 86 | notify(jsonObject); 87 | } catch (Exception e){ 88 | e.printStackTrace(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/Base64.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | 5 | final class Base64 { 6 | private Base64() { 7 | } 8 | 9 | public static byte[] decode(String in) { 10 | // Ignore trailing '=' padding and whitespace from the input. 11 | int limit = in.length(); 12 | for (; limit > 0; limit--) { 13 | char c = in.charAt(limit - 1); 14 | if (c != '=' && c != '\n' && c != '\r' && c != ' ' && c != '\t') { 15 | break; 16 | } 17 | } 18 | 19 | // If the input includes whitespace, this output array will be longer than necessary. 20 | byte[] out = new byte[(int) (limit * 6L / 8L)]; 21 | int outCount = 0; 22 | int inCount = 0; 23 | 24 | int word = 0; 25 | for (int pos = 0; pos < limit; pos++) { 26 | char c = in.charAt(pos); 27 | 28 | int bits; 29 | if (c >= 'A' && c <= 'Z') { 30 | // char ASCII value 31 | // A 65 0 32 | // Z 90 25 (ASCII - 65) 33 | bits = c - 65; 34 | } else if (c >= 'a' && c <= 'z') { 35 | // char ASCII value 36 | // a 97 26 37 | // z 122 51 (ASCII - 71) 38 | bits = c - 71; 39 | } else if (c >= '0' && c <= '9') { 40 | // char ASCII value 41 | // 0 48 52 42 | // 9 57 61 (ASCII + 4) 43 | bits = c + 4; 44 | } else if (c == '+' || c == '-') { 45 | bits = 62; 46 | } else if (c == '/' || c == '_') { 47 | bits = 63; 48 | } else if (c == '\n' || c == '\r' || c == ' ' || c == '\t') { 49 | continue; 50 | } else { 51 | return null; 52 | } 53 | 54 | // Append this char's 6 bits to the word. 55 | word = (word << 6) | (byte) bits; 56 | 57 | // For every 4 chars of input, we accumulate 24 bits of output. Emit 3 bytes. 58 | inCount++; 59 | if (inCount % 4 == 0) { 60 | out[outCount++] = (byte) (word >> 16); 61 | out[outCount++] = (byte) (word >> 8); 62 | out[outCount++] = (byte) word; 63 | } 64 | } 65 | 66 | int lastWordChars = inCount % 4; 67 | if (lastWordChars == 1) { 68 | // We read 1 char followed by "===". But 6 bits is a truncated byte! Fail. 69 | return null; 70 | } else if (lastWordChars == 2) { 71 | // We read 2 chars followed by "==". Emit 1 byte with 8 of those 12 bits. 72 | word = word << 12; 73 | out[outCount++] = (byte) (word >> 16); 74 | } else if (lastWordChars == 3) { 75 | // We read 3 chars, followed by "=". Emit 2 bytes for 16 of those 18 bits. 76 | word = word << 6; 77 | out[outCount++] = (byte) (word >> 16); 78 | out[outCount++] = (byte) (word >> 8); 79 | } 80 | 81 | // If we sized our out array perfectly, we're done. 82 | if (outCount == out.length) return out; 83 | 84 | // Copy the decoded bytes to a new, right-sized array. 85 | byte[] prefix = new byte[outCount]; 86 | System.arraycopy(out, 0, prefix, 0, outCount); 87 | return prefix; 88 | } 89 | 90 | private static final byte[] MAP = new byte[] { 91 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 92 | 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 93 | 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', 94 | '5', '6', '7', '8', '9', '+', '/' 95 | }; 96 | 97 | private static final byte[] URL_MAP = new byte[] { 98 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 99 | 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 100 | 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', 101 | '5', '6', '7', '8', '9', '-', '_' 102 | }; 103 | 104 | public static String encode(byte[] in) { 105 | return encode(in, MAP); 106 | } 107 | 108 | public static String encodeUrl(byte[] in) { 109 | return encode(in, URL_MAP); 110 | } 111 | 112 | private static String encode(byte[] in, byte[] map) { 113 | int length = (in.length + 2) * 4 / 3; 114 | byte[] out = new byte[length]; 115 | int index = 0, end = in.length - in.length % 3; 116 | for (int i = 0; i < end; i += 3) { 117 | out[index++] = map[(in[i] & 0xff) >> 2]; 118 | out[index++] = map[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; 119 | out[index++] = map[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; 120 | out[index++] = map[(in[i + 2] & 0x3f)]; 121 | } 122 | switch (in.length % 3) { 123 | case 1: 124 | out[index++] = map[(in[end] & 0xff) >> 2]; 125 | out[index++] = map[(in[end] & 0x03) << 4]; 126 | out[index++] = '='; 127 | out[index++] = '='; 128 | break; 129 | case 2: 130 | out[index++] = map[(in[end] & 0xff) >> 2]; 131 | out[index++] = map[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; 132 | out[index++] = map[((in[end + 1] & 0x0f) << 2)]; 133 | out[index++] = '='; 134 | break; 135 | } 136 | try { 137 | return new String(out, 0, index, "US-ASCII"); 138 | } catch (UnsupportedEncodingException e) { 139 | throw new AssertionError(e); 140 | } 141 | } 142 | } -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/EncodeUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | import java.net.URLDecoder; 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.HashMap; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by zl on 16/1/30. 12 | */ 13 | class EncodeUtils { 14 | 15 | public static final String DEFAULT_CHARSET = "UTF-8"; 16 | 17 | /** 18 | * sha1 19 | * @param decript 20 | * @return 21 | */ 22 | public static byte[] sha1(String decript) { 23 | try { 24 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 25 | digest.update(decript.getBytes(DEFAULT_CHARSET)); 26 | return digest.digest(); 27 | } catch (NoSuchAlgorithmException e) { 28 | e.printStackTrace(); 29 | } catch (UnsupportedEncodingException e) { 30 | e.printStackTrace(); 31 | } 32 | return null; 33 | } 34 | 35 | public static byte[] sha1(String... strs) { 36 | try { 37 | MessageDigest digest = MessageDigest.getInstance("SHA-1"); 38 | for (String str : strs) { 39 | digest.update(str.getBytes(DEFAULT_CHARSET)); 40 | } 41 | return digest.digest(); 42 | } catch (NoSuchAlgorithmException e) { 43 | e.printStackTrace(); 44 | } catch (UnsupportedEncodingException e) { 45 | e.printStackTrace(); 46 | } 47 | return null; 48 | } 49 | 50 | 51 | /** 52 | * 提权get 请求参数 53 | * 54 | * @param s 类似 aa=1&bb=2&cc=3 55 | * @return [key, value] 56 | */ 57 | //TODO // FIXME: 16/1/31 这里为了简单,没有完全遵守http规范,只取了query对应value的第一个值 58 | public static Map parseUrlQueryString(String s) { 59 | if (s == null) return new HashMap(0); 60 | 61 | HashMap map1 = new HashMap(); 62 | int p = 0; 63 | while (p < s.length()) { 64 | int p0 = p; 65 | while (p < s.length() && s.charAt(p) != '=' && s.charAt(p) != '&') p++; 66 | String name = urlDecode(s.substring(p0, p)); 67 | if (p < s.length() && s.charAt(p) == '=') p++; 68 | p0 = p; 69 | while (p < s.length() && s.charAt(p) != '&') p++; 70 | String value = urlDecode(s.substring(p0, p)); 71 | if (p < s.length() && s.charAt(p) == '&') p++; 72 | if(value!=null){ 73 | map1.put(name, value.trim()); 74 | } 75 | 76 | } 77 | return map1; 78 | } 79 | 80 | 81 | // public static Map parseUrlQueryString(String s) { 82 | // if (s == null) return new HashMap(0); 83 | // // In map1 we use strings and ArrayLists to collect the parameter values. 84 | // HashMap map1 = new HashMap(); 85 | // int p = 0; 86 | // while (p < s.length()) { 87 | // int p0 = p; 88 | // while (p < s.length() && s.charAt(p) != '=' && s.charAt(p) != '&') p++; 89 | // String name = urlDecode(s.substring(p0, p)); 90 | // if (p < s.length() && s.charAt(p) == '=') p++; 91 | // p0 = p; 92 | // while (p < s.length() && s.charAt(p) != '&') p++; 93 | // String value = urlDecode(s.substring(p0, p)); 94 | // if (p < s.length() && s.charAt(p) == '&') p++; 95 | // Object x = map1.get(name); 96 | // if (x == null) { 97 | // // The first value of each name is added directly as a string to the map. 98 | // map1.put(name, value); 99 | // } else if (x instanceof String) { 100 | // // For multiple values, we use an ArrayList. 101 | // ArrayList a = new ArrayList(); 102 | // a.add((String) x); 103 | // a.add(value); 104 | // map1.put(name, a); 105 | // } else { 106 | // @SuppressWarnings("unchecked") 107 | // ArrayList a = (ArrayList) x; 108 | // a.add(value); 109 | // } 110 | // } 111 | // // Copy map1 to map2. Map2 uses string arrays to store the parameter values. 112 | // HashMap map2 = new HashMap(map1.size()); 113 | // for (Map.Entry e : map1.entrySet()) { 114 | // String name = e.getKey(); 115 | // Object x = e.getValue(); 116 | // String[] v; 117 | // if (x instanceof String) { 118 | // v = new String[]{(String) x}; 119 | // } else { 120 | // @SuppressWarnings("unchecked") 121 | // ArrayList a = (ArrayList) x; 122 | // v = new String[a.size()]; 123 | // v = a.toArray(v); 124 | // } 125 | // map2.put(name, v); 126 | // } 127 | // return map2; 128 | // } 129 | 130 | private static String urlDecode(String s) { 131 | try { 132 | return URLDecoder.decode(s, "UTF-8"); 133 | } catch (UnsupportedEncodingException e) { 134 | throw new RuntimeException("Error in urlDecode.", e); 135 | } 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/HttpResponse.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import android.os.Environment; 4 | 5 | import org.json.JSONArray; 6 | import org.json.JSONException; 7 | import org.json.JSONObject; 8 | 9 | import java.io.File; 10 | import java.io.FileInputStream; 11 | import java.io.IOException; 12 | import java.io.OutputStream; 13 | import java.net.Socket; 14 | import java.util.ArrayList; 15 | import java.util.Arrays; 16 | import java.util.Collections; 17 | import java.util.HashMap; 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.Set; 21 | 22 | /** 23 | * Created by zl on 16/1/31. 24 | */ 25 | public class HttpResponse { 26 | 27 | private static final String REQUEST_ACTION = "action"; 28 | private static List sResponseHandles; 29 | 30 | public static void handle(Socket socket, Map headerMap) throws IOException { 31 | if (sResponseHandles == null) { 32 | synchronized (HttpResponse.class) { 33 | sResponseHandles = new ArrayList() {{ 34 | add(new ListFileResponseHandle()); 35 | add(new DownloadFileResponseHandle()); 36 | }}; 37 | } 38 | } 39 | 40 | String getUrl = headerMap.get(WebSocket.REQUEST_LINE); 41 | //只处理了基本的请求 42 | int start = getUrl.indexOf('?'); 43 | int end = getUrl.lastIndexOf("HTTP/1.1"); 44 | if (start != -1 && end != -1) { 45 | Map queryString = EncodeUtils.parseUrlQueryString(getUrl.substring(start + 1, end)); 46 | String action = queryString.get(REQUEST_ACTION); 47 | if (action != null) { 48 | for (ResponseHandle handle : sResponseHandles) { 49 | if (handle.isMatchAction(action)) { 50 | handle.hanlde(queryString, socket.getOutputStream()); 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | 57 | } 58 | 59 | 60 | //error response 61 | private static void responseInnerError(OutputStream outputStream, int code, String msg, String content, Throwable e) throws IOException { 62 | StringBuilder sb = new StringBuilder("HTTP/1.1 ").append(code).append(msg).append("\r\n"); 63 | sb.append("\r\n\r\n"); 64 | if (content != null) { 65 | sb.append(content).append("\r\n"); 66 | } 67 | if (e != null) { 68 | sb.append(e.getMessage()); 69 | } 70 | outputStream.write(sb.toString().getBytes()); 71 | outputStream.flush(); 72 | } 73 | 74 | private interface ResponseHandle { 75 | boolean isMatchAction(String action); 76 | 77 | void hanlde(Map query, OutputStream outputStream) throws IOException; 78 | } 79 | 80 | private static abstract class AbsResponseHandle implements ResponseHandle { 81 | 82 | void writeHeaders(OutputStream outputStream, Map headers) throws IOException { 83 | StringBuilder sb = new StringBuilder("HTTP/1.1 200 OK \r\n"); 84 | 85 | if (headers != null) { 86 | Set> entries = headers.entrySet(); 87 | for (Map.Entry entry : entries) { 88 | sb.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n"); 89 | } 90 | sb.append("\r\n"); 91 | } else { 92 | sb.append("\r\n\r\n"); 93 | } 94 | outputStream.write(sb.toString().getBytes()); 95 | } 96 | 97 | boolean formatJsonp(Map query){ 98 | return "jsonp".equals(query.get("format"))&&query.containsKey("callback"); 99 | } 100 | } 101 | 102 | 103 | private static class ListFileResponseHandle extends AbsResponseHandle { 104 | 105 | static final File DEFAULT_DIR=Environment.getExternalStorageDirectory(); 106 | 107 | @Override 108 | public boolean isMatchAction(String action) { 109 | return "list".equals(action); 110 | } 111 | 112 | @Override 113 | public void hanlde(Map query, OutputStream outputStream) throws IOException { 114 | String dir = query.get("dir"); 115 | File file = (dir == null ? DEFAULT_DIR : new File(dir)); 116 | 117 | if (!file.exists()) { 118 | file = DEFAULT_DIR; 119 | } 120 | 121 | String[] list = file.list(); 122 | String output=""; 123 | try { 124 | if(list != null) { 125 | ArrayList dirs=new ArrayList(); 126 | ArrayList files=new ArrayList(); 127 | for (String path:list){ 128 | File file1=new File(file.getAbsolutePath(),path); 129 | if(file1.isDirectory()){ 130 | dirs.add(file1.getName()); 131 | }else if(file1.isFile()){ 132 | files.add(file1.getName()); 133 | } 134 | } 135 | 136 | JSONObject jsonObject = new JSONObject(); 137 | jsonObject.put("dir", file.getAbsolutePath()); 138 | 139 | jsonObject.put("file_list", new JSONArray(files)); 140 | jsonObject.put("dir_list",new JSONArray(dirs)); 141 | 142 | if (formatJsonp(query)) { 143 | output = query.get("callback") + "(" + jsonObject.toString() + ")"; 144 | } else { 145 | output = jsonObject.toString(); 146 | } 147 | } 148 | } catch (Exception e) { 149 | e.printStackTrace(); 150 | } 151 | 152 | byte[] data = output.getBytes(); 153 | 154 | Map headers = new HashMap(); 155 | headers.put("Content-Type", "application/json"); 156 | headers.put("Content-Length", "" + data.length); 157 | 158 | writeHeaders(outputStream, headers); 159 | outputStream.write(data); 160 | outputStream.flush(); 161 | 162 | } 163 | } 164 | 165 | private static class DownloadFileResponseHandle extends AbsResponseHandle { 166 | 167 | @Override 168 | public boolean isMatchAction(String action) { 169 | return "download".equals(action); 170 | } 171 | 172 | @Override 173 | public void hanlde(Map query, OutputStream outputStream) throws IOException { 174 | String path = query.get("path"); 175 | if (path == null) { 176 | path = Environment.getExternalStorageDirectory().getAbsolutePath(); 177 | } 178 | File file = new File(path); 179 | if (file.exists() && file.isFile()) { 180 | respFile(file, outputStream); 181 | } else { 182 | responseInnerError(outputStream, 404, "Not Found", "file " + path + " not found !", null); 183 | } 184 | } 185 | 186 | private void respFile(File file, OutputStream outputStream) throws IOException { 187 | Map headers = new HashMap(); 188 | headers.put("Content-Type", "application/octet-stream"); 189 | headers.put("Content-Length", "" + file.length()); 190 | headers.put("Content-Disposition", "attachment;filename=\"" + file.getName() + "\""); 191 | writeHeaders(outputStream, headers); 192 | 193 | FileInputStream fis = null; 194 | try { 195 | fis = new FileInputStream(file); 196 | byte[] buff = new byte[8192]; 197 | int len = -1; 198 | while ((len = fis.read(buff)) != -1) { 199 | outputStream.write(buff, 0, len); 200 | } 201 | outputStream.flush(); 202 | } catch (IOException e) { 203 | e.printStackTrace(); 204 | } finally { 205 | try { 206 | if (fis != null) { 207 | fis.close(); 208 | } 209 | } catch (IOException e) { 210 | e.printStackTrace(); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/LineInputStream.java: -------------------------------------------------------------------------------- 1 | /* 2 | * LineInputStream.java 3 | * Copyright 2014 Patrick Meade. 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU Affero General Public License as published by 7 | * the Free Software Foundation, either version 3 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU Affero General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU Affero General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | package com.zzzmode.android.server; 20 | 21 | import java.io.ByteArrayOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStream; 24 | 25 | 26 | /** 27 | * LineInputStream decorates an InputStream for line-by-line reading. 28 | * It implements a readLine() method, similar to 29 | * BufferedReader, but does not need an intermediate 30 | * InputStreamReader to translate. 31 | * 32 | *

The reason we avoid an InputStreamReader is that it 33 | * will consume as much of the input as it possibly can in order 34 | * to optimize character encoding from the input bytes. 35 | * 36 | *

In our case, the input will be switching protocols from an HTTP 37 | * Request to WebSocket frames. We want to consume the HTTP Request 38 | * line by line and process it for a WebSocket handshake, but the 39 | * data following that must be handled in a completely different way. 40 | * 41 | *

The ability to handle the data line-by-line like 42 | * BufferedReader is able to do, is very useful for 43 | * parsing the HTTP Reqeust. Therefore we have ListInputStream to 44 | * provide this functionality as an InputStream decorator. 45 | * 46 | *

Unlike BufferedReader, which handles three kinds of 47 | * line endings (CR, LF, CRLF), the HTTP protocol has defined the line 48 | * ending to be CRLF. Therefore, the readLine() method of 49 | * this decorator requires a CRLF (or EOF) sequence to terminate a line. 50 | * 51 | * @author blinkdog 52 | */ 53 | public class LineInputStream extends InputStream { 54 | /** 55 | * Constant defining Carriage Return (CR). Octet 13, Hex 0x0c. 56 | */ 57 | public static final int CR = 13; 58 | 59 | /** 60 | * Constant defining the end of the stream (EOF). This is derived 61 | * from the InputStream API. Calls to read() return -1 62 | * when the end of the stream is reached. 63 | */ 64 | public static final int EOF = -1; 65 | 66 | /** 67 | * Constant defining Line Feed (LF). Octet 10, Hex 0x0a 68 | */ 69 | public static final int LF = 10; 70 | 71 | /** 72 | * Constant defining the canonical name of the UTF-8 character encoding. 73 | */ 74 | public static final String UTF_8 = "UTF-8"; 75 | 76 | /** 77 | * Construct a LineInputStream to decorate the provided InputStream. 78 | * A NullPointerException will be thrown if the provided 79 | * InputStream is null. 80 | * @param in InputStream to be decorated by this LineInputStream 81 | */ 82 | public LineInputStream(final InputStream in) { 83 | //checkNotNull(in); 84 | this.inputStream = in; 85 | } 86 | 87 | /** 88 | * {@inheritDoc} 89 | * @return the next byte of data, or -1 if the end of the 90 | * stream is reached. 91 | * @throws IOException if an I/O error occurs 92 | */ 93 | @Override 94 | public final int read() throws IOException { 95 | return inputStream.read(); 96 | } 97 | 98 | /** 99 | * Reads a line of text. A line is considered to be terminated by a 100 | * carriage return ('\r') followed immediately by a linefeed ('\n'). 101 | * This is per the HTTP specification. 102 | * @return String containing the contents of the line, not including 103 | * any line-termination characters, or null if the end of the 104 | * stream has been reached 105 | * @throws IOException if an I/O error occurs 106 | */ 107 | public final String readLine() throws IOException { 108 | ByteArrayOutputStream baos = new ByteArrayOutputStream(); 109 | boolean inputTaken = false; 110 | while (true) { 111 | int data = inputStream.read(); 112 | // if this is the end of the stream 113 | if (data == EOF) { 114 | // if we've taken some input already 115 | if (inputTaken) { 116 | // return that input 117 | return baos.toString(UTF_8); 118 | } else { 119 | // otherwise return null 120 | return null; 121 | } 122 | // otherwise, if this is a CR 123 | } else if (data == CR) { 124 | // it may be the end of a line 125 | lastWasCarriageReturn = true; 126 | // otherwise, if this is a LF 127 | } else if (data == LF) { 128 | // if we did follow a CR 129 | if (lastWasCarriageReturn) { 130 | // then this is the end of a line 131 | lastWasCarriageReturn = false; 132 | return baos.toString(UTF_8); 133 | } else { 134 | inputTaken = true; 135 | lastWasCarriageReturn = false; 136 | baos.write(LF); 137 | } 138 | // otherwise... 139 | } else { 140 | // if the last thing was a carriage return 141 | if (lastWasCarriageReturn) { 142 | // write that CR to our line 143 | baos.write(CR); 144 | } 145 | // add the data we just read to the line 146 | inputTaken = true; 147 | lastWasCarriageReturn = false; 148 | baos.write(data); 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * InputStream to be decorated by this LineInputStream. This reference 155 | * is provided at construction time. 156 | */ 157 | private final InputStream inputStream; 158 | 159 | /** 160 | * Flag: Is the last character we processed a Carriage Return (Octet 13)? 161 | * true: Yes, the last character was a CR 162 | * false: No, the last character was not a CR 163 | */ 164 | private boolean lastWasCarriageReturn = false; 165 | } 166 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/NetworkUtils.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import java.net.*; 4 | import java.util.Enumeration; 5 | import java.util.List; 6 | 7 | /** 8 | * Created by zl on 16/1/26. 9 | */ 10 | public class NetworkUtils { 11 | 12 | public static int prefixLengthToNetmaskInt(int prefixLength) 13 | throws IllegalArgumentException { 14 | if (prefixLength < 0 || prefixLength > 32) { 15 | throw new IllegalArgumentException("Invalid prefix length (0 <= prefix <= 32)"); 16 | } 17 | int value = 0xffffffff << (32 - prefixLength); 18 | return Integer.reverseBytes(value); 19 | } 20 | 21 | public static int netmaskIntToPrefixLength(int netmask) { 22 | return Integer.bitCount(netmask); 23 | } 24 | 25 | public static int inetAddressToInt(Inet4Address inetAddr) 26 | throws IllegalArgumentException { 27 | byte [] addr = inetAddr.getAddress(); 28 | return ((addr[3] & 0xff) << 24) | ((addr[2] & 0xff) << 16) | 29 | ((addr[1] & 0xff) << 8) | (addr[0] & 0xff); 30 | } 31 | 32 | 33 | public static InetAddress intToInetAddress(int hostAddress) { 34 | byte[] addressBytes = { (byte)(0xff & hostAddress), 35 | (byte)(0xff & (hostAddress >> 8)), 36 | (byte)(0xff & (hostAddress >> 16)), 37 | (byte)(0xff & (hostAddress >> 24)) }; 38 | try { 39 | return InetAddress.getByAddress(addressBytes); 40 | } catch (UnknownHostException e) { 41 | e.printStackTrace(); 42 | } 43 | return null; 44 | } 45 | 46 | public static int[] intToArray(int i) { 47 | return new int[]{i & 255, i >> 8 & 255, i >> 16 & 255, i >> 24 & 255}; 48 | } 49 | 50 | 51 | public static InetAddress getLocalHostLANAddress() { 52 | try { 53 | InetAddress candidateAddress = null; 54 | for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) { 55 | NetworkInterface iface = (NetworkInterface) ifaces.nextElement(); 56 | 57 | for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements();) { 58 | InetAddress inetAddr = (InetAddress) inetAddrs.nextElement(); 59 | if (inetAddr instanceof Inet4Address && !inetAddr.isLoopbackAddress()) { 60 | 61 | if (inetAddr.isSiteLocalAddress()) { 62 | return inetAddr; 63 | } 64 | else if (candidateAddress == null) { 65 | candidateAddress = inetAddr; 66 | } 67 | } 68 | } 69 | } 70 | if (candidateAddress != null) { 71 | return candidateAddress; 72 | } 73 | return InetAddress.getLocalHost(); 74 | } 75 | catch (Exception e) { 76 | e.printStackTrace(); 77 | } 78 | 79 | return null; 80 | } 81 | 82 | 83 | 84 | public static int getNetworkPrefixLength(InetAddress address){ 85 | try { 86 | final NetworkInterface networkInterface = NetworkInterface.getByInetAddress(address); 87 | final List interfaceAddresses = networkInterface.getInterfaceAddresses(); 88 | for (InterfaceAddress addr:interfaceAddresses){ 89 | int len=addr.getNetworkPrefixLength(); 90 | if(len > 0 && len < 32){ 91 | return len; 92 | } 93 | } 94 | } catch (SocketException e) { 95 | e.printStackTrace(); 96 | } 97 | return 0; 98 | } 99 | } -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/WebSocket.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import java.io.IOException; 4 | 5 | /** 6 | * Created by zl on 16/1/31. 7 | */ 8 | public interface WebSocket { 9 | public static final String REQUEST_LINE = "REQUEST_LINE"; 10 | 11 | public static final int LENGTH_16 = 0x7E; 12 | 13 | public static final int LENGTH_16_MIN = 126; 14 | 15 | 16 | public static final int LENGTH_64 = 0x7F; 17 | 18 | 19 | public static final int LENGTH_64_MIN = 0x10000; 20 | 21 | 22 | public static final int MASK_HIGH_WORD_HIGH_BYTE_NO_SIGN = 0x7f000000; 23 | 24 | 25 | public static final int MASK_HIGH_WORD_LOW_BYTE = 0x00ff0000; 26 | 27 | 28 | public static final int MASK_LOW_WORD_HIGH_BYTE = 0x0000ff00; 29 | 30 | 31 | public static final int MASK_LOW_WORD_LOW_BYTE = 0x000000ff; 32 | 33 | 34 | public static final int OCTET_ONE = 8; 35 | 36 | 37 | public static final int OCTET_TWO = 16; 38 | 39 | 40 | public static final int OCTET_THREE = 24; 41 | 42 | 43 | public static final int OPCODE_FRAME_BINARY = 0x82; 44 | 45 | 46 | public static final int OPCODE_FRAME_CLOSE = 0x88; 47 | 48 | 49 | public static final int OPCODE_FRAME_PONG = 0x8A; 50 | 51 | 52 | public static final int OPCODE_FRAME_TEXT = 0x81; 53 | 54 | public static final String WEBSOCKET_ACCEPT_UUID = 55 | "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 56 | 57 | 58 | public static final String DEFAULT_CHARSET = "UTF-8"; 59 | 60 | 61 | void send(byte[] bytes) throws IOException; 62 | void send(String text)throws IOException; 63 | byte[] readFrame()throws IOException; 64 | void close() throws IOException; 65 | boolean isClosed(); 66 | boolean isConnected(); 67 | 68 | void setWebSocketCallback(WebSocketCallback callback); 69 | 70 | public interface WebSocketCallback{ 71 | void onReceivedFrame(byte[] bytes); 72 | void onClosed(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/WebSocketImpl.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.io.OutputStream; 6 | import java.io.UnsupportedEncodingException; 7 | import java.net.Socket; 8 | import java.util.Map; 9 | 10 | /** 11 | * Created by zl on 16/1/31. 12 | */ 13 | class WebSocketImpl implements WebSocket { 14 | 15 | private Socket mSocket; 16 | private OutputStream outputStream; 17 | private InputStream inputStream; 18 | 19 | private boolean handshakeComplete = false; 20 | private boolean closed = false; 21 | private boolean closeSent = false; 22 | 23 | private WebSocketCallback mCallback; 24 | 25 | public WebSocketImpl(Socket socket,Map headers) throws IOException { 26 | this.mSocket = socket; 27 | init(headers); 28 | } 29 | 30 | private void init(Map headers) throws IOException { 31 | inputStream = mSocket.getInputStream(); 32 | outputStream = mSocket.getOutputStream(); 33 | shakeHands(headers); 34 | } 35 | 36 | //先处理websocket握手 37 | private void shakeHands(Map headers) throws IOException { 38 | //Map headers = parseHeader(inputStream); 39 | String requestLine = headers.get(REQUEST_LINE); 40 | 41 | handshakeComplete = checkStartsWith(requestLine, "GET /") 42 | && checkContains(requestLine, "HTTP/") 43 | && headers.get("Host") != null 44 | && checkContains(headers.get("upgrade"), "websocket") 45 | && checkContains(headers.get("connection"), "Upgrade") 46 | && "13".equals(headers.get("sec-websocket-version")) 47 | && headers.get("sec-websocket-key") != null; 48 | String nonce = headers.get("sec-websocket-key"); 49 | //根据客户端请求的seckey生成一个acceptkey, 50 | // 具体做法是先提取seckey+[websocket公开的一个uuid,这个uuid是固定的] 51 | // 拼接成一个字符串计算sha1值 52 | 53 | if (handshakeComplete) { 54 | byte[] nonceBytes = Base64.decode(nonce); 55 | if (nonceBytes == null ||nonceBytes.length != 16) { 56 | handshakeComplete = false; 57 | } 58 | } 59 | // if we have met all the requirements 60 | if (handshakeComplete) { 61 | //返回请求头,表示握手完成,之后每次以frame为单位传输数据 62 | outputStream.write(asUTF8("HTTP/1.1 101 Switching Protocols\r\n")); 63 | outputStream.write(asUTF8("Upgrade: websocket\r\n")); 64 | outputStream.write(asUTF8("Connection: upgrade\r\n")); 65 | outputStream.write(asUTF8("Sec-WebSocket-Accept: ")); 66 | byte[] hashByte = EncodeUtils.sha1(nonce, WEBSOCKET_ACCEPT_UUID); 67 | String acceptKey = Base64.encode(hashByte); 68 | outputStream.write(asUTF8(acceptKey)); 69 | outputStream.write(asUTF8("\r\n\r\n")); 70 | } 71 | 72 | } 73 | 74 | private void readFully(byte[] b) throws IOException { 75 | int readen = 0; 76 | while (readen < b.length) { 77 | int r = inputStream.read(b, readen, b.length - readen); 78 | if (r == -1) 79 | break; 80 | readen += r; 81 | } 82 | } 83 | 84 | @Override 85 | public byte[] readFrame() throws IOException { 86 | 87 | int opcode = inputStream.read(); 88 | //boolean whole = (opcode & 0b10000000) !=0; 89 | opcode = opcode & 0xF; 90 | 91 | if (opcode != 1) { 92 | finshSocket(); 93 | throw new IOException("Wrong opcode: " + opcode); 94 | } 95 | //读取消息内容长度的有多种情况 96 | int len = inputStream.read(); 97 | boolean encoded = (len >= 128); 98 | 99 | if (encoded) { 100 | len -= 128; 101 | } 102 | 103 | if (len == 127) { 104 | len = (inputStream.read() << 16) | (inputStream.read() << 8) | inputStream.read(); 105 | } else if (len == 126) { 106 | len = (inputStream.read() << 8) | inputStream.read(); 107 | } 108 | 109 | byte[] key = null; 110 | 111 | if (encoded) { 112 | key = new byte[4]; 113 | readFully(key); 114 | } 115 | 116 | byte[] frame = new byte[len]; 117 | 118 | readFully(frame); 119 | 120 | if (encoded) { 121 | for (int i = 0; i < frame.length; i++) { 122 | frame[i] = (byte) (frame[i] ^ key[i % 4]); 123 | } 124 | } 125 | if (mCallback != null) { 126 | mCallback.onReceivedFrame(frame); 127 | } 128 | return frame; 129 | } 130 | 131 | 132 | private void writeData(byte[] bytes, int opcode) throws IOException { 133 | try { 134 | int binLength = bytes.length; 135 | outputStream.write(opcode); 136 | if (binLength < LENGTH_16_MIN) { 137 | outputStream.write(binLength); 138 | } else if (binLength < LENGTH_64_MIN) { 139 | outputStream.write(LENGTH_16); 140 | outputStream.write( 141 | (binLength & MASK_LOW_WORD_HIGH_BYTE) >> OCTET_ONE); 142 | outputStream.write(binLength & MASK_LOW_WORD_LOW_BYTE); 143 | } else { 144 | outputStream.write(LENGTH_64); 145 | outputStream.write(0x00); 146 | outputStream.write(0x00); 147 | outputStream.write(0x00); 148 | outputStream.write(0x00); 149 | outputStream.write( 150 | (binLength & MASK_HIGH_WORD_HIGH_BYTE_NO_SIGN) >> OCTET_THREE); 151 | outputStream.write( 152 | (binLength & MASK_HIGH_WORD_LOW_BYTE) >> OCTET_TWO); 153 | outputStream.write( 154 | (binLength & MASK_LOW_WORD_HIGH_BYTE) >> OCTET_ONE); 155 | outputStream.write(binLength & MASK_LOW_WORD_LOW_BYTE); 156 | } 157 | outputStream.write(bytes); 158 | } catch (IOException e) { 159 | finshSocket(); 160 | throw e; 161 | } 162 | } 163 | 164 | public final void writeClose() throws IOException { 165 | if (!closeSent) { 166 | closeSent = true; 167 | outputStream.write(new byte[]{ 168 | (byte) OPCODE_FRAME_CLOSE, (byte) 0x00 169 | }); 170 | } 171 | } 172 | 173 | 174 | @Override 175 | public void send(byte[] bytes) throws IOException { 176 | writeData(bytes, OPCODE_FRAME_BINARY); 177 | } 178 | 179 | @Override 180 | public void send(String text) throws IOException { 181 | byte[] utfBytes = asUTF8(text); 182 | writeData(utfBytes, OPCODE_FRAME_TEXT); 183 | } 184 | 185 | @Override 186 | public void close() { 187 | finshSocket(); 188 | } 189 | 190 | public boolean isClosed() { 191 | return closed || closeSent; 192 | } 193 | 194 | @Override 195 | public boolean isConnected() { 196 | return handshakeComplete; 197 | } 198 | 199 | @Override 200 | public void setWebSocketCallback(WebSocketCallback callback) { 201 | mCallback = callback; 202 | } 203 | 204 | 205 | public final void finshSocket() { 206 | closed = true; 207 | handshakeComplete = false; 208 | 209 | if (mCallback != null) { 210 | mCallback.onClosed(); 211 | } 212 | 213 | try { 214 | if (inputStream != null) { 215 | inputStream.close(); 216 | } 217 | } catch (IOException e) { 218 | e.printStackTrace(); 219 | } 220 | try { 221 | if (outputStream != null) { 222 | outputStream.close(); 223 | } 224 | } catch (IOException e) { 225 | e.printStackTrace(); 226 | } 227 | try { 228 | if (mSocket != null) { 229 | mSocket.close(); 230 | } 231 | } catch (IOException e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | 236 | 237 | 238 | public static byte[] asUTF8(final String s) { 239 | try { 240 | return s.getBytes(DEFAULT_CHARSET); 241 | } catch (UnsupportedEncodingException e) { 242 | e.printStackTrace(); 243 | } 244 | return new byte[0]; 245 | } 246 | 247 | public static boolean checkContains(final String s1, final String s2) { 248 | return s1 != null && s1.contains(s2); 249 | } 250 | 251 | public static boolean checkStartsWith(final String s1, final String s2) { 252 | return s1 != null && s1.startsWith(s2); 253 | } 254 | 255 | } 256 | -------------------------------------------------------------------------------- /remotelogcat/src/main/java/com/zzzmode/android/server/WebSocketServer.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.server; 2 | 3 | import android.util.Log; 4 | 5 | import com.zzzmode.android.internel.ThreadUtil; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.lang.ref.WeakReference; 10 | import java.net.InetAddress; 11 | import java.net.ServerSocket; 12 | import java.net.Socket; 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | /** 20 | * Created by zl on 16/1/31. 21 | */ 22 | public class WebSocketServer { 23 | 24 | private ServerSocket mServerSocket; 25 | private WebSocketServerCallback mCallback; 26 | private int port=0; 27 | private volatile boolean isActive=false; 28 | 29 | private boolean wsCanRead=true; 30 | 31 | private List> mListWebSocket=new ArrayList>(); 32 | private String mWebSocketPrefix; 33 | 34 | public WebSocketServer(int port,String prefix) throws IOException { 35 | this.port=port; 36 | mWebSocketPrefix=prefix; 37 | } 38 | 39 | public void setWsCanRead(boolean b){ 40 | wsCanRead=b; 41 | } 42 | 43 | public void setWebSocketServerCallback(WebSocketServerCallback mCallback) { 44 | this.mCallback = mCallback; 45 | } 46 | 47 | 48 | public void start(){ 49 | if(isActive){ 50 | return; 51 | } 52 | 53 | ThreadUtil.getExecutor().execute(new Runnable() { 54 | @Override 55 | public void run() { 56 | innerStart(); 57 | } 58 | }); 59 | 60 | } 61 | 62 | private void innerStart(){ 63 | try { 64 | mServerSocket=new ServerSocket(port); 65 | 66 | final InetAddress hostLANAddress = NetworkUtils.getLocalHostLANAddress(); 67 | if(hostLANAddress != null){ 68 | Log.e("WebSocketServer", "Server start success! Connection IP : "+hostLANAddress.getHostAddress()+":"+port); 69 | }else { 70 | Log.e("WebSocketServer", "Server start success! But unknow local ip address !"); 71 | } 72 | 73 | isActive=true; 74 | while (isActive){ 75 | handleSocket(mServerSocket.accept()); 76 | } 77 | } catch (Exception e) { 78 | if(mCallback != null){ 79 | mCallback.onClosed(); 80 | } 81 | 82 | e.printStackTrace(); 83 | } 84 | } 85 | 86 | 87 | public void stop(){ 88 | isActive=false; 89 | try { 90 | for (WeakReference webSockets:mListWebSocket){ 91 | try{ 92 | WebSocket socket = webSockets.get(); 93 | if(socket != null){ 94 | socket.close(); 95 | } 96 | }catch (Exception e){ 97 | } 98 | } 99 | 100 | if(mCallback != null){ 101 | mCallback.onClosed(); 102 | } 103 | 104 | if(mServerSocket!=null){ 105 | mServerSocket.close(); 106 | } 107 | 108 | } catch (IOException e) { 109 | e.printStackTrace(); 110 | } 111 | } 112 | 113 | private void handleSocket(final Socket socket) { 114 | if(!isActive){ 115 | return; 116 | } 117 | boolean handled=false; 118 | try { 119 | boolean isHttp=false; 120 | boolean isWebsocket=false; 121 | final Map headerMap=parseHeader(socket.getInputStream()); 122 | String s = headerMap.get(WebSocket.REQUEST_LINE); 123 | if(s!= null && s.startsWith("GET /")){ 124 | isHttp=true; 125 | isWebsocket=s.startsWith("GET "+mWebSocketPrefix); 126 | } 127 | final Map headers=headerMap; 128 | 129 | if(isWebsocket){ 130 | 131 | ThreadUtil.getExecutor().execute(new Runnable() { 132 | @Override 133 | public void run() { 134 | handleAsWebsocket(socket,headers); 135 | } 136 | }); 137 | handled=true; 138 | }else if(isHttp){ 139 | ThreadUtil.getExecutor().execute(new Runnable() { 140 | @Override 141 | public void run() { 142 | handleAsHttp(socket,headers); 143 | } 144 | }); 145 | handled=true; 146 | } 147 | 148 | } catch (IOException e) { 149 | e.printStackTrace(); 150 | }finally { 151 | try { 152 | //未知协议,不处理 153 | if (!handled) { 154 | socket.close(); 155 | } 156 | } catch (IOException e) { 157 | e.printStackTrace(); 158 | } 159 | } 160 | } 161 | 162 | 163 | private void handleAsWebsocket(final Socket socket, final Map headerMap) { 164 | 165 | try { 166 | 167 | WebSocket webSocket = new WebSocketImpl(socket, headerMap); 168 | mListWebSocket.add(new WeakReference(webSocket)); 169 | 170 | if (mCallback != null) { 171 | mCallback.onConnected(webSocket); 172 | } 173 | 174 | while (wsCanRead && !webSocket.isClosed()) { 175 | try { 176 | webSocket.readFrame(); 177 | } catch (IOException e) { 178 | e.printStackTrace(); 179 | } 180 | } 181 | } catch (IOException e) { 182 | e.printStackTrace(); 183 | } 184 | 185 | } 186 | 187 | 188 | private void handleAsHttp(final Socket socket, final Map headerMap) { 189 | 190 | try { 191 | HttpResponse.handle(socket, headerMap); 192 | socket.getOutputStream().flush(); 193 | } catch (IOException e) { 194 | e.printStackTrace(); 195 | } finally { 196 | try { 197 | if (socket != null) { 198 | socket.close(); 199 | } 200 | } catch (IOException e) { 201 | e.printStackTrace(); 202 | } 203 | } 204 | } 205 | 206 | 207 | public static Map parseHeader(InputStream inputStream) { 208 | LineInputStream lis = new LineInputStream(inputStream); 209 | Map headerMap = new HashMap(); 210 | try { 211 | String line = lis.readLine(); 212 | while (line != null && line.isEmpty()) { 213 | line = lis.readLine(); 214 | } 215 | headerMap.put(WebSocket.REQUEST_LINE, line); 216 | line = lis.readLine(); 217 | while (line != null && !line.isEmpty()) { 218 | int firstColonPos = line.indexOf(":"); 219 | if (firstColonPos > 0) { 220 | String key = line.substring(0, firstColonPos).trim(); 221 | int length = line.length(); 222 | String value = line.substring(firstColonPos + 1, length).trim(); 223 | if (!key.isEmpty() && !value.isEmpty()) { 224 | headerMap.put(key, value); 225 | headerMap.put(key.toLowerCase(), value); 226 | } 227 | } 228 | line = lis.readLine(); 229 | } 230 | } catch (IOException e) { 231 | e.printStackTrace(); 232 | } 233 | return Collections.unmodifiableMap(headerMap); 234 | } 235 | 236 | public interface WebSocketServerCallback{ 237 | void onConnected(WebSocket webSocket); 238 | void onClosed(); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /remotelogcat/src/test/java/com/zzzmode/android/remotelogcat/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcat; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * To work on unit tests, switch the Test Artifact in the Build Variants view. 9 | */ 10 | public class ExampleUnitTest { 11 | @Test 12 | public void addition_isCorrect() throws Exception { 13 | assertEquals(4, 2 + 2); 14 | } 15 | } -------------------------------------------------------------------------------- /remotelogcatsample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /remotelogcatsample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 27 5 | buildToolsVersion "27.0.3" 6 | 7 | defaultConfig { 8 | applicationId "com.zzzmode.android.remotelogcatsample" 9 | minSdkVersion 15 10 | targetSdkVersion 27 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 | implementation fileTree(dir: 'libs', include: ['*.jar']) 24 | testImplementation 'junit:junit:4.12' 25 | implementation 'com.android.support:appcompat-v7:23.1.1' 26 | implementation project(':remotelogcat') 27 | } 28 | -------------------------------------------------------------------------------- /remotelogcatsample/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/zl/develop/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 | -------------------------------------------------------------------------------- /remotelogcatsample/src/androidTest/java/com/zzzmode/android/remotelogcatsample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcatsample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /remotelogcatsample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/java/com/zzzmode/android/remotelogcatsample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.zzzmode.android.remotelogcatsample; 2 | 3 | import android.os.Bundle; 4 | import android.os.SystemClock; 5 | import android.support.v7.app.AppCompatActivity; 6 | import android.util.Log; 7 | 8 | import com.zzzmode.android.remotelogcat.LogcatRunner; 9 | 10 | import java.io.IOException; 11 | import java.util.Random; 12 | 13 | public class MainActivity extends AppCompatActivity { 14 | 15 | private static final String TAG = "MainActivity"; 16 | 17 | private boolean logRunning = false; 18 | 19 | @Override 20 | protected void onCreate(Bundle savedInstanceState) { 21 | super.onCreate(savedInstanceState); 22 | setContentView(R.layout.activity_main); 23 | 24 | try { 25 | LogcatRunner.getInstance() 26 | .config(LogcatRunner.LogConfig.builder() 27 | .setWsCanReceiveMsg(false) 28 | .write2File(true)) 29 | .with(getApplicationContext()) 30 | .start(); 31 | } catch (IOException e) { 32 | e.printStackTrace(); 33 | } 34 | } 35 | 36 | private void startLog() { 37 | if (logRunning) { 38 | return; 39 | } 40 | logRunning = true; 41 | new Thread(new Runnable() { 42 | @Override 43 | public void run() { 44 | Random random = new Random(); 45 | int i = 0; 46 | while (logRunning) { 47 | if (random.nextBoolean()) { 48 | Log.e("testlog", "run --> " + i); 49 | } else { 50 | Log.w("testlog", "run --> " + i); 51 | 52 | } 53 | // test();test();test();test();test();test();test(); 54 | // test();test();test();test();test();test();test(); 55 | SystemClock.sleep(random.nextInt(5000) + 100); 56 | i++; 57 | } 58 | } 59 | }).start(); 60 | 61 | } 62 | 63 | private static void test(){ 64 | try { 65 | throw new RuntimeException("----"); 66 | }catch (Exception e){ 67 | e.printStackTrace(); 68 | } 69 | } 70 | 71 | @Override 72 | protected void onStart() { 73 | super.onStart(); 74 | startLog(); 75 | } 76 | 77 | @Override 78 | protected void onStop() { 79 | super.onStop(); 80 | logRunning = false; 81 | } 82 | 83 | 84 | @Override 85 | protected void onDestroy() { 86 | super.onDestroy(); 87 | LogcatRunner.getInstance().stop(); 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/remotelogcatsample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/remotelogcatsample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/remotelogcatsample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/remotelogcatsample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/8enet/RemoteLogcatViewer/2c0a06a5ff3c2702854699b7db2d1d4b875b1697/remotelogcatsample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | RemoteLogcatSample 3 | MainActivity 4 | 5 | -------------------------------------------------------------------------------- /remotelogcatsample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |