├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── kk │ │ └── screencapture │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── kk │ │ │ └── screencapture │ │ │ ├── CaptureServer.java │ │ │ ├── ForegroundScreencapService.java │ │ │ ├── MainActivity.java │ │ │ ├── live │ │ │ ├── LiveKeeper.java │ │ │ ├── ScreenBroadcastListener.java │ │ │ ├── ScreenManager.java │ │ │ └── WindowHelper.java │ │ │ ├── model │ │ │ ├── ActionMessage.java │ │ │ └── ArkResponse.java │ │ │ └── util │ │ │ ├── BitmapUtil.java │ │ │ ├── FixedLenQueue.java │ │ │ ├── IOUtil.java │ │ │ ├── MapUtil.java │ │ │ └── ThreadUtil.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── kk │ └── screencapture │ ├── CaptureServerTest.java │ ├── ExampleUnitTest.java │ └── FixedLenQueueTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lombok.config ├── settings.gradle └── snaps ├── app-ui.jpg ├── snap.png ├── start.png └── usb-open.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | .DS_Store 5 | build 6 | /captures 7 | .externalNativeBuild 8 | 9 | .idea 10 | tmp 11 | 12 | release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XScreencapper Android截屏大师 2 | 3 | 这是一个轻量、开发友好的Android手机截屏工具,提供多种自定义的指令,从而满足不同的场景,例如**监控**、**群控**等等。 4 | 5 | 兼容Android5.0+以上的主流Android手机,提供优质、持续的截图流,相比目前的主流截图方案,不仅比adb稳定可靠,更支持最高30FPS的稳定截图流,可满足大部分的使用场景。 6 | 7 | #### 效果图 8 | 9 | ![效果图](snaps/snap.png) 10 | 11 | >### 支持的指令 12 | 13 | #### start 14 | 15 | 启动截屏服务 16 | 17 | #### stop 18 | 19 | 关闭截屏服务 20 | 21 | #### size 22 | 23 | 改变截屏尺寸大小,如 `360x640` 24 | 25 | #### frequency 26 | 27 | 改变截屏频率,以毫秒值为单位,如 `1000` 代表每秒截一次 28 | 29 | #### quality 30 | 31 | 图片质量,支持范围 `40-100` 32 | 33 | 34 | >### 部署 35 | 36 | - 克隆客户端工程 **`XScreencapperClient`** 37 | 38 | 该工程是连接截图服务、获取截图流的client端实现,目前主要支持Python、Java和NodeJs版本 39 | 40 | ``` 41 | git clone https://github.com/flash-kk/XScreencapperClient.git 42 | ``` 43 | 44 | - 手机通过USB接入电脑,并打开**`开发者选项`**和**`USB调试`** 45 | 46 | ![开启USB调试](snaps/usb-open.jpg) 47 | 48 | - 使用adb命令打开端口转发 49 | 50 | ``` 51 | cd XScreencapperClient/scripts 52 | ./reset-forword.sh 9999 53 | ``` 54 | 55 | - 安装`XScreencapper-v1.0.0.apk`到手机,点击启动后,默认会监听在手机的`9999`端口 56 | 57 | 注意该应用无界面,仅显示一个前台通知 58 | 59 | ![app通知](snaps/app-ui.jpg) 60 | 61 | - 启动python客户端工程 62 | 63 | ``` 64 | cd XScreencapperClient/web 65 | python server.py 66 | ``` 67 | 68 | - 打开[首页](http://0.0.0.0:5000/static/index.html),效果图 69 | 70 | #### 起始页 71 | 72 | ![起始页](snaps/start.png) 73 | 74 | #### 截屏页 75 | 76 | ![截屏](snaps/snap.png) -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.kk.screencapturewithprojection" 7 | minSdkVersion 23 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | 13 | javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } } 14 | } 15 | 16 | packagingOptions { 17 | exclude 'META-INF/*' 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | } 27 | 28 | dependencies { 29 | implementation fileTree(dir: 'libs', include: ['*.jar']) 30 | implementation 'com.android.support:appcompat-v7:28.0.0' 31 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 32 | 33 | testImplementation 'junit:junit:4.12' 34 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 35 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 36 | 37 | // Adds libraries defining annotations to only the compile classpath. 38 | compileOnly 'com.google.dagger:dagger:2.11' 39 | // Adds the annotation processor dependency to the annotation processor classpath. 40 | annotationProcessor 'com.google.dagger:dagger-compiler:2.11' 41 | 42 | compileOnly "org.projectlombok:lombok:1.16.18" 43 | implementation 'org.glassfish:javax.annotation:10.0-b28' 44 | 45 | implementation group: 'com.corundumstudio.socketio', name: 'netty-socketio', version: '1.7.17' 46 | 47 | implementation 'com.google.code.gson:gson:2.8.5' 48 | } 49 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/kk/screencapture/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import android.support.test.runner.AndroidJUnit4; 4 | 5 | import com.google.gson.Gson; 6 | import com.kk.screencapture.model.ActionMessage; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * @see Testing documentation 15 | */ 16 | @RunWith(AndroidJUnit4.class) 17 | public class ExampleInstrumentedTest { 18 | @Test 19 | public void useAppContext() { 20 | String actionStr = "{\"action\":\"size\",\"extra\":{\"w\":100}}"; 21 | 22 | ActionMessage actionMessage = new Gson().fromJson(actionStr, ActionMessage.class); 23 | System.out.print(actionStr); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/CaptureServer.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import android.graphics.Bitmap; 4 | import android.media.Image; 5 | import android.util.Log; 6 | 7 | import com.corundumstudio.socketio.AckRequest; 8 | import com.corundumstudio.socketio.Configuration; 9 | import com.corundumstudio.socketio.SocketConfig; 10 | import com.corundumstudio.socketio.SocketIOClient; 11 | import com.corundumstudio.socketio.SocketIOServer; 12 | import com.corundumstudio.socketio.listener.ConnectListener; 13 | import com.corundumstudio.socketio.listener.DataListener; 14 | import com.corundumstudio.socketio.listener.DisconnectListener; 15 | import com.corundumstudio.socketio.transport.NamespaceClient; 16 | import com.google.gson.Gson; 17 | import com.kk.screencapture.model.ActionMessage; 18 | import com.kk.screencapture.model.ArkResponse; 19 | import com.kk.screencapture.util.BitmapUtil; 20 | import com.kk.screencapture.util.FixedLenQueue; 21 | import com.kk.screencapture.util.ThreadUtil; 22 | 23 | import java.nio.ByteBuffer; 24 | import java.util.ArrayList; 25 | import java.util.HashMap; 26 | import java.util.HashSet; 27 | import java.util.List; 28 | import java.util.Map; 29 | import java.util.Random; 30 | import java.util.Set; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.concurrent.Executors; 33 | import java.util.concurrent.atomic.AtomicInteger; 34 | import java.util.concurrent.atomic.AtomicLong; 35 | 36 | /** 37 | * 截屏服务器 38 | * 负责将截图传递给pc端的客户端套接字,并接收客户端指令,修改截图频率、质量等等 39 | **/ 40 | public class CaptureServer { 41 | private static final String TAG = "SockIOUtil"; 42 | 43 | private static final String EVENT_MESSAGE = "message"; 44 | 45 | private static final String EVENT_ACTION = "action"; 46 | 47 | private static final String EVENT_BMP_BASE64 = "bmp_base64"; 48 | 49 | /** 50 | * 支持的动作选项 51 | **/ 52 | private static final String ACTION_START = "start"; 53 | private static final String ACTION_STOP = "stop"; 54 | private static final String ACTION_SIZE = "size"; // 改变截图尺寸 55 | private static final String ACTION_FRENQUENCY = "frequency"; // 改变采样频率 56 | private static final String ACTION_QUALITY = "quality"; // 图片质量,默认90 57 | 58 | private int port = 9999; 59 | 60 | private SocketIOServer socketIOServer; 61 | private Configuration conf; 62 | 63 | ForegroundScreencapService screencapService; 64 | FixedLenQueue imgBufferedQueue; 65 | ExecutorService executorService = Executors.newFixedThreadPool(30); 66 | 67 | Map clientMap = new HashMap<>(); 68 | Set workingSidSet = new HashSet<>(); 69 | List sidList = new ArrayList<>(); 70 | 71 | AtomicLong sendCounter = new AtomicLong(); 72 | 73 | public CaptureServer(ForegroundScreencapService screencapService) { 74 | this.screencapService = screencapService; 75 | this.imgBufferedQueue = screencapService.imgBufferedQueue; 76 | 77 | } 78 | 79 | private static void logI(String log) { 80 | Log.i(TAG, log); 81 | } 82 | 83 | public void startDaemon() { 84 | logI("start server port:" + port); 85 | 86 | SocketConfig socketConfig = new SocketConfig(); 87 | socketConfig.setReuseAddress(true); 88 | socketConfig.setTcpKeepAlive(true); 89 | socketConfig.setTcpNoDelay(true); 90 | socketConfig.setSoLinger(0); 91 | 92 | conf = new Configuration(); 93 | conf.setPort(port); 94 | conf.setSocketConfig(socketConfig); 95 | 96 | socketIOServer = new SocketIOServer(conf); 97 | 98 | ServerListenerAdapter serverListenerAdapter = new ServerListenerAdapter(); 99 | 100 | socketIOServer.addConnectListener(serverListenerAdapter); 101 | socketIOServer.addDisconnectListener(serverListenerAdapter); 102 | 103 | socketIOServer.addEventListener(EVENT_ACTION, ActionMessage.class, serverListenerAdapter); 104 | 105 | socketIOServer.start(); 106 | } 107 | 108 | /** 109 | * 循环广播缓存队列的图片 110 | **/ 111 | public void loopBroadcastCaptures() { 112 | ThreadUtil.doRun(new BmpSendRunnable()); 113 | } 114 | 115 | long lastSize = 0; 116 | 117 | public class BmpSendRunnable implements Runnable { 118 | @Override 119 | public void run() { 120 | while (true) { 121 | // 阻塞式获取最新的图片 122 | Image capturedImg = imgBufferedQueue.take(); 123 | 124 | final long index = sendCounter.addAndGet(1); 125 | logI(String.format("prepare to send bmp:%s,queue:%s", index, imgBufferedQueue.size())); 126 | 127 | Bitmap bmp = imgToBmp(capturedImg); 128 | capturedImg.close(); 129 | 130 | String base64Img = BitmapUtil.bitmapToString(bmp); 131 | 132 | if (!bmp.isRecycled()) { 133 | bmp.recycle(); 134 | } 135 | 136 | // 微小差异不处理 137 | int curSize = base64Img.length(); 138 | 139 | if (Math.abs(curSize - lastSize) < 10) { 140 | Log.w(TAG, "ignore tiny diff"); 141 | continue; 142 | } 143 | 144 | lastSize = curSize; 145 | 146 | base64Img = index + "::data:image/webp;base64," + base64Img; 147 | 148 | final int size = sidList.size(); 149 | 150 | if (size == 0) { 151 | Log.w(TAG, "no alive client available!"); 152 | continue; 153 | } 154 | 155 | final String finalBase64Img = base64Img; 156 | executorService.submit(new Runnable() { 157 | @Override 158 | public void run() { 159 | String sid = chooseConnnectedClient(); 160 | 161 | if (null == sid) { 162 | Log.w(TAG, "fail to send bmp for lack of idle client"); 163 | return; 164 | } 165 | 166 | workingSidSet.add(sid); 167 | 168 | logI(String.format("index:%s,broadcasting base64 image:%s", 169 | index, finalBase64Img.length())); 170 | 171 | clientMap.get(sid).sendEvent(EVENT_BMP_BASE64, finalBase64Img); 172 | 173 | workingSidSet.remove(sid); 174 | } 175 | }); 176 | } 177 | } 178 | } 179 | 180 | /** 181 | * 获取一个连接的客户端 182 | **/ 183 | public String chooseConnnectedClient() { 184 | Random randomNum = new Random(); 185 | int maxFail = 10; 186 | while (true) { 187 | int choseClientIndex = randomNum.nextInt(sidList.size()); 188 | 189 | NamespaceClient client = clientMap.get(sidList.get(choseClientIndex)); 190 | 191 | if (client.getBaseClient().isConnected()) { 192 | return sidList.get(choseClientIndex); 193 | } 194 | 195 | maxFail--; 196 | if (maxFail < 0) { 197 | return null; 198 | } 199 | } 200 | } 201 | 202 | /** 203 | * image转换为Bitmap 204 | **/ 205 | private Bitmap imgToBmp(Image image) { 206 | int width = image.getWidth(); 207 | int height = image.getHeight(); 208 | 209 | final Image.Plane[] planes = image.getPlanes(); 210 | final ByteBuffer buffer = planes[0].getBuffer(); 211 | int pixelStride = planes[0].getPixelStride(); 212 | int rowStride = planes[0].getRowStride(); 213 | int rowPadding = rowStride - pixelStride * width; 214 | 215 | Bitmap bmp = Bitmap.createBitmap(width + rowPadding / pixelStride, height, Bitmap.Config.ARGB_8888); 216 | bmp.copyPixelsFromBuffer(buffer); 217 | bmp = Bitmap.createBitmap(bmp, 0, 0, width, height); 218 | 219 | return bmp; 220 | } 221 | 222 | /** 223 | * 释放 224 | **/ 225 | public void release() { 226 | if (null != socketIOServer) { 227 | socketIOServer.stop(); 228 | socketIOServer = null; 229 | } 230 | } 231 | 232 | /** 233 | * 监听器适配器 234 | **/ 235 | private class ServerListenerAdapter implements ConnectListener, DisconnectListener, DataListener { 236 | @Override 237 | public void onConnect(SocketIOClient client) { 238 | String sid = client.getSessionId().toString(); 239 | 240 | if (sidList.contains(sid)) { 241 | return; 242 | } 243 | 244 | sidList.add(sid); 245 | clientMap.put(sid, (NamespaceClient) client); 246 | 247 | loopBroadcastCaptures(); 248 | 249 | logI(String.format("connect,sid:%s,sid count:%s,client count:%s", 250 | client.getSessionId(), sidList.size(), socketIOServer.getAllClients().size())); 251 | 252 | client.sendEvent(EVENT_MESSAGE, "i am server"); 253 | } 254 | 255 | @Override 256 | public void onDisconnect(SocketIOClient client) { 257 | String sid = client.getSessionId().toString(); 258 | 259 | if (!sidList.contains(sid)) return; 260 | 261 | sidList.remove(sid); 262 | clientMap.remove(sid); 263 | 264 | logI(String.format("disconnect,sid:%s,sid count:%s,client count:%s", 265 | client.getSessionId(), sidList.size(), socketIOServer.getAllClients().size())); 266 | } 267 | 268 | AtomicInteger actionId = new AtomicInteger(); 269 | 270 | long lastActionTime = 0; 271 | int id = 0; 272 | long minActionIntervalMillis = 10 * 1000; // 最少间隔10s以上才接收下一个指令 273 | 274 | @Override 275 | public void onData(SocketIOClient client, ActionMessage actionMsg, AckRequest ackSender) throws Exception { 276 | if (0 == lastActionTime) { 277 | lastActionTime = System.currentTimeMillis(); 278 | } 279 | 280 | if (System.currentTimeMillis() - lastActionTime < minActionIntervalMillis) { 281 | respBad(ackSender, "cannot do action too frequently!"); 282 | return; 283 | } 284 | 285 | Log.i(TAG, "recv action from client:" + actionMsg); 286 | 287 | switch (actionMsg.getAction()) { 288 | case ACTION_START: 289 | screencapService.startCaptureProcess(); 290 | break; 291 | 292 | case ACTION_STOP: 293 | screencapService.stopCaptureProcess(); 294 | break; 295 | 296 | case ACTION_SIZE: 297 | if (actionMsg.getWidth() <= 0) { 298 | respBad(ackSender, "width cannot be <=0"); 299 | return; 300 | } 301 | 302 | if (actionMsg.getHeight() <= 0) { 303 | respBad(ackSender, "height cannot be <=0"); 304 | return; 305 | } 306 | 307 | screencapService.mScreenWidth = actionMsg.getWidth(); 308 | screencapService.mScreenHeight = actionMsg.getHeight(); 309 | 310 | screencapService.restartCaptureProcess(); 311 | break; 312 | 313 | case ACTION_FRENQUENCY: 314 | if (actionMsg.getFrequency() <= 0) { 315 | respBad(ackSender, "frequency cannot be <= 0"); 316 | return; 317 | } 318 | 319 | screencapService.captureIntervalMillis = actionMsg.getFrequency(); 320 | screencapService.restartCaptureProcess(); 321 | break; 322 | 323 | case ACTION_QUALITY: 324 | int quality = actionMsg.getQuality(); 325 | 326 | if (quality > 100 || quality < 40) { 327 | respBad(ackSender, "quality must between 40 and 100"); 328 | return; 329 | } 330 | 331 | BitmapUtil.quality = quality; 332 | break; 333 | 334 | default: 335 | String msg = "action is not supported,only suppport:start | stop | size | frequency"; 336 | Log.w(TAG, msg); 337 | 338 | respBad(ackSender, msg); 339 | return; 340 | } 341 | 342 | respOk(ackSender); 343 | } 344 | 345 | private void respOk(AckRequest ackSender) { 346 | if (null != ackSender) { 347 | ackSender.sendAckData(new Gson().toJson(ArkResponse.ok())); 348 | } 349 | } 350 | 351 | private void respBad(AckRequest ackSender, String msg) { 352 | if (null != ackSender) { 353 | ackSender.sendAckData(new Gson().toJson(ArkResponse.bad(msg))); 354 | } 355 | } 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/ForegroundScreencapService.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import android.app.Activity; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.app.Service; 8 | import android.content.Context; 9 | import android.content.Intent; 10 | import android.content.SyncContext; 11 | import android.graphics.BitmapFactory; 12 | import android.graphics.PixelFormat; 13 | import android.hardware.display.DisplayManager; 14 | import android.hardware.display.VirtualDisplay; 15 | import android.media.Image; 16 | import android.media.ImageReader; 17 | import android.media.projection.MediaProjection; 18 | import android.media.projection.MediaProjectionManager; 19 | import android.os.Build; 20 | import android.os.IBinder; 21 | import android.support.annotation.RequiresApi; 22 | import android.support.v4.app.NotificationCompat; 23 | import android.util.DisplayMetrics; 24 | import android.util.Log; 25 | import android.view.WindowManager; 26 | 27 | import com.kk.screencapture.util.FixedLenQueue; 28 | import com.kk.screencapture.util.IOUtil; 29 | import com.kk.screencapture.util.ThreadUtil; 30 | 31 | import java.util.Timer; 32 | import java.util.TimerTask; 33 | import java.util.concurrent.atomic.AtomicLong; 34 | 35 | /** 36 | * 前台截图进程 37 | **/ 38 | public class ForegroundScreencapService extends Service { 39 | private static final String TAG = "ScreencapService"; 40 | 41 | // 防止多次调用start 42 | private volatile boolean isStarted = false; 43 | public volatile boolean isCapturing = false; 44 | 45 | // 启动 46 | public static final String ACTION_START = "start"; 47 | // 退出 48 | public static final String ACTION_STOP = "stop"; 49 | 50 | private static final String CHANNEL_ID = "screen cap"; 51 | private static final int NOTI_ID = 1; 52 | private static final String CHANNEL_NAME = "X-Screencap"; 53 | private static final String CHANNEL_DESCRIPTION = "live screen capping..."; 54 | 55 | WindowManager mWindowManager; 56 | public int mScreenWidth = 270; 57 | public int mScreenHeight = 480; 58 | private int mScreenDensity; 59 | 60 | MediaProjectionManager mMediaProjectionManager; 61 | private MediaProjection mMediaProjection; 62 | private VirtualDisplay mVirtualDisplay; 63 | private ImageReader mImageReader; 64 | 65 | public static Intent captureDataIntent; 66 | 67 | Timer captureTimer = null; 68 | TimerTask captureTask = null; 69 | 70 | // 默认截图间隔 71 | public long captureIntervalMillis = 1; 72 | private int maxBufferedSize = 60; 73 | 74 | FixedLenQueue imgBufferedQueue = new FixedLenQueue<>(maxBufferedSize); 75 | CaptureServer captureServer; 76 | 77 | @Override 78 | public void onCreate() { 79 | super.onCreate(); 80 | 81 | // 获取屏幕信息 82 | getScreenInfo(); 83 | 84 | mMediaProjectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE); 85 | 86 | captureServer = new CaptureServer(this); 87 | } 88 | 89 | @Override 90 | public IBinder onBind(Intent intent) { 91 | return null; 92 | } 93 | 94 | @Override 95 | public int onStartCommand(Intent intent, int flags, int startId) { 96 | if (intent == null) { 97 | return super.onStartCommand(intent, flags, startId); 98 | } 99 | 100 | String action = intent.getAction(); 101 | 102 | if (action == null) { 103 | tryStartCapture(); 104 | return START_STICKY; 105 | } 106 | 107 | switch (action) { 108 | case ACTION_START: 109 | tryStartCapture(); 110 | break; 111 | 112 | case ACTION_STOP: 113 | stop(); 114 | } 115 | 116 | return START_STICKY; 117 | } 118 | 119 | @Override 120 | public void onDestroy() { 121 | super.onDestroy(); 122 | 123 | releaseCaptureReader(); 124 | 125 | if (mMediaProjection != null) { 126 | mMediaProjection.stop(); 127 | mMediaProjection = null; 128 | } 129 | 130 | captureServer.release(); 131 | 132 | stopCaptureTimer(); 133 | 134 | isStarted = false; 135 | } 136 | 137 | /** 138 | * 创建通知的渠道 139 | **/ 140 | @RequiresApi(Build.VERSION_CODES.O) 141 | private void createChannel() { 142 | NotificationManager mNotificationManager = (NotificationManager) this.getSystemService(NOTIFICATION_SERVICE); 143 | if (mNotificationManager == null) { 144 | return; 145 | } 146 | 147 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_DEFAULT); 148 | channel.setDescription(CHANNEL_DESCRIPTION); 149 | channel.setShowBadge(true); 150 | channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); 151 | 152 | mNotificationManager.createNotificationChannel(channel); 153 | } 154 | 155 | /** 156 | * 启动前台服务 157 | **/ 158 | private void startForegroundService() { 159 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 160 | createChannel(); 161 | } 162 | 163 | NotificationCompat.BigTextStyle bigTextStyle = new NotificationCompat.BigTextStyle(); 164 | bigTextStyle.setBigContentTitle(CHANNEL_NAME); 165 | bigTextStyle.bigText(CHANNEL_DESCRIPTION); 166 | 167 | // 前台服务的通知 168 | Notification foreNoti = new NotificationCompat.Builder(this, CHANNEL_ID) 169 | .setStyle(bigTextStyle) 170 | .setWhen(System.currentTimeMillis()) 171 | .setSmallIcon(R.mipmap.ic_launcher) 172 | .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) 173 | .build(); 174 | 175 | startForeground(NOTI_ID, foreNoti); 176 | } 177 | 178 | private void stop() { 179 | stopForeground(true); 180 | stopSelf(); 181 | } 182 | 183 | /** 184 | * 获取屏幕信息 185 | **/ 186 | private void getScreenInfo() { 187 | mWindowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE); 188 | 189 | DisplayMetrics metrics = new DisplayMetrics(); 190 | 191 | mWindowManager.getDefaultDisplay().getMetrics(metrics); 192 | mScreenDensity = metrics.densityDpi; 193 | } 194 | 195 | /** 196 | * 启动截屏服务 197 | **/ 198 | private void tryStartCapture() { 199 | if (isStarted) { 200 | return; 201 | } 202 | 203 | if (null == captureDataIntent) { 204 | Log.e(TAG, "capture intent cannot be empty"); 205 | stop(); 206 | return; 207 | } 208 | 209 | startForegroundService(); 210 | 211 | Log.i(TAG, "starting socketio server..."); 212 | captureServer.startDaemon(); 213 | captureServer.loopBroadcastCaptures(); 214 | 215 | mMediaProjection = mMediaProjectionManager.getMediaProjection(Activity.RESULT_OK, captureDataIntent); 216 | 217 | // 启动截屏进程 218 | startCaptureProcess(); 219 | 220 | isStarted = true; 221 | } 222 | 223 | /** 224 | * 启动截屏进程 225 | **/ 226 | public void startCaptureProcess() { 227 | if (isCapturing) { 228 | return; 229 | } 230 | 231 | Log.i(TAG, "starting capture process..."); 232 | 233 | // 创建截屏读取器 234 | createCaptureReader(); 235 | 236 | // 启动截图定时器 237 | startCaptureTimer(); 238 | 239 | Log.i(TAG, "success to start capture process"); 240 | 241 | isCapturing = true; 242 | } 243 | 244 | /** 245 | * 停止截屏 246 | **/ 247 | public void stopCaptureProcess() { 248 | if (!isCapturing) { 249 | return; 250 | } 251 | 252 | Log.i(TAG, "stoping capture process..."); 253 | stopCaptureTimer(); 254 | 255 | releaseCaptureReader(); 256 | 257 | imgBufferedQueue.queue.clear(); 258 | 259 | Log.i(TAG, "success to stop capture process"); 260 | 261 | isCapturing = false; 262 | } 263 | 264 | public void restartCaptureProcess() { 265 | stopCaptureProcess(); 266 | 267 | ThreadUtil.doRunDelayed(new Runnable() { 268 | @Override 269 | public void run() { 270 | startCaptureProcess(); 271 | } 272 | }, 3); 273 | } 274 | 275 | /** 276 | * 创建截屏读取器 277 | **/ 278 | private void createCaptureReader() { 279 | if (null == mImageReader) { 280 | mImageReader = ImageReader.newInstance(mScreenWidth, mScreenHeight, PixelFormat.RGBA_8888, maxBufferedSize); 281 | } 282 | 283 | if (null == mVirtualDisplay) { 284 | mVirtualDisplay = mMediaProjection.createVirtualDisplay("ScreenCapture", 285 | mScreenWidth, mScreenHeight, mScreenDensity, 286 | DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, 287 | mImageReader.getSurface(), null, null); 288 | } 289 | } 290 | 291 | /** 292 | * 销毁截屏读取器 293 | **/ 294 | private void releaseCaptureReader() { 295 | if (null != mImageReader) { 296 | mImageReader.close(); 297 | mImageReader = null; 298 | } 299 | 300 | if (null != mVirtualDisplay) { 301 | mVirtualDisplay.release(); 302 | mVirtualDisplay = null; 303 | } 304 | } 305 | 306 | /** 307 | * 启动截图定时器 308 | **/ 309 | public void startCaptureTimer() { 310 | // 停止旧的任务 311 | stopCaptureTimer(); 312 | 313 | captureTimer = new Timer(); 314 | captureTask = new TimerTask() { 315 | @Override 316 | public void run() { 317 | capture(); 318 | } 319 | }; 320 | 321 | captureTimer.scheduleAtFixedRate(captureTask, 3000, captureIntervalMillis); 322 | } 323 | 324 | /** 325 | * 停止旧的定时器 326 | **/ 327 | public void stopCaptureTimer() { 328 | if (null != captureTask) { 329 | captureTask.cancel(); 330 | captureTask = null; 331 | } 332 | 333 | if (null != captureTimer) { 334 | captureTimer.cancel(); 335 | captureTimer.purge(); 336 | captureTimer = null; 337 | } 338 | } 339 | 340 | AtomicLong captureCount = new AtomicLong(); 341 | 342 | private void capture() { 343 | try { 344 | int queueSize = imgBufferedQueue.size(); 345 | 346 | if (maxBufferedSize - queueSize < 5) { 347 | ThreadUtil.sleepQuitelySecs(3); 348 | Log.w(TAG, String.format("capture queue is almost full:%s", queueSize)); 349 | return; 350 | } 351 | 352 | if (captureServer.sidList.size() == 0) { 353 | Log.w(TAG, "no client connect!!!"); 354 | ThreadUtil.sleepQuitelySecs(10); 355 | return; 356 | } 357 | 358 | Image capturedImg = mImageReader.acquireLatestImage(); 359 | 360 | if (capturedImg == null) { 361 | return; 362 | } 363 | 364 | long count = captureCount.addAndGet(1); 365 | 366 | Image oldestImg = imgBufferedQueue.offer(capturedImg); 367 | 368 | Log.i(TAG, String.format("capture count:%s,queue:%s", count, imgBufferedQueue.size())); 369 | 370 | IOUtil.closeQuitely(oldestImg); 371 | } catch (Exception e) { 372 | e.printStackTrace(); 373 | } 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.media.projection.MediaProjectionManager; 7 | import android.os.Bundle; 8 | import android.support.annotation.Nullable; 9 | import android.support.v7.app.AppCompatActivity; 10 | import android.util.Log; 11 | import android.widget.Toast; 12 | 13 | import com.kk.screencapture.live.LiveKeeper; 14 | import com.kk.screencapture.live.WindowHelper; 15 | import com.kk.screencapture.util.ThreadUtil; 16 | 17 | public class MainActivity extends AppCompatActivity { 18 | public static final String TAG = "MainActivity"; 19 | 20 | private final int REQUEST_MEDIA_PROJECTION = 0; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | // WindowHelper.set1x1Window(getWindow()); 27 | 28 | // LiveKeeper.keepLive(this); 29 | 30 | ThreadUtil.doRunDelayed(new Runnable() { 31 | @Override 32 | public void run() { 33 | Log.i(TAG, "request for capture service"); 34 | startActivityForResult(((MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE)).createScreenCaptureIntent(), 35 | REQUEST_MEDIA_PROJECTION); 36 | } 37 | }, 3 * 1000); 38 | } 39 | 40 | @Override 41 | protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { 42 | if (requestCode != REQUEST_MEDIA_PROJECTION || resultCode != Activity.RESULT_OK) { 43 | super.onActivityResult(requestCode, resultCode, data); 44 | tooltip("截取失败"); 45 | } 46 | 47 | ForegroundScreencapService.captureDataIntent = data; 48 | 49 | startService(new Intent(MainActivity.this, ForegroundScreencapService.class)); 50 | 51 | finish(); 52 | } 53 | 54 | /** 55 | * tooltip 56 | **/ 57 | public void tooltip(String msg) { 58 | Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); 59 | } 60 | } 61 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/live/LiveKeeper.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.live; 2 | 3 | import android.support.v7.app.AppCompatActivity; 4 | import android.util.Log; 5 | 6 | import com.kk.screencapture.MainActivity; 7 | 8 | public class LiveKeeper { 9 | public static final String TAG = "LiveKeeper"; 10 | 11 | public static void keepLive(AppCompatActivity activity) { 12 | final ScreenManager screenManager = ScreenManager.getInstance(activity); 13 | screenManager.setActivity(activity); 14 | 15 | ScreenBroadcastListener listener = new ScreenBroadcastListener(activity); 16 | 17 | listener.registerListener(new ScreenBroadcastListener.ScreenStateListener() { 18 | @Override 19 | public void onScreenOn() { 20 | // screenManager.finishActivity(); 21 | } 22 | 23 | @Override 24 | public void onScreenOff() { 25 | Log.w(TAG, "try to start placeholder activity"); 26 | screenManager.startActivity(MainActivity.class); 27 | } 28 | }); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/live/ScreenBroadcastListener.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.live; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.util.Log; 8 | 9 | public class ScreenBroadcastListener { 10 | public static final String TAG = "ScreenBroadcastListener"; 11 | 12 | private Context mContext; 13 | 14 | private ScreenBroadcastReceiver mScreenReceiver; 15 | 16 | private ScreenStateListener mListener; 17 | 18 | public ScreenBroadcastListener(Context context) { 19 | mContext = context.getApplicationContext(); 20 | 21 | mScreenReceiver = new ScreenBroadcastReceiver(); 22 | } 23 | 24 | public void registerListener(ScreenStateListener listener) { 25 | mListener = listener; 26 | registerListener(); 27 | } 28 | 29 | private void registerListener() { 30 | IntentFilter filter = new IntentFilter(); 31 | 32 | filter.addAction(Intent.ACTION_SCREEN_ON); 33 | filter.addAction(Intent.ACTION_SCREEN_OFF); 34 | 35 | mContext.registerReceiver(mScreenReceiver, filter); 36 | } 37 | 38 | /** 39 | * screen状态广播接收者 40 | */ 41 | private class ScreenBroadcastReceiver extends BroadcastReceiver { 42 | private String action = null; 43 | 44 | @Override 45 | public void onReceive(Context context, Intent intent) { 46 | action = intent.getAction(); 47 | 48 | switch (action) { 49 | case Intent.ACTION_SCREEN_ON: 50 | mListener.onScreenOn(); 51 | break; 52 | 53 | case Intent.ACTION_SCREEN_OFF: 54 | mListener.onScreenOff(); 55 | break; 56 | 57 | default: 58 | Log.w(TAG, "unsupport action!"); 59 | } 60 | } 61 | } 62 | 63 | public interface ScreenStateListener { 64 | void onScreenOn(); 65 | 66 | void onScreenOff(); 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/live/ScreenManager.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.live; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | import java.lang.ref.WeakReference; 8 | 9 | public class ScreenManager { 10 | 11 | private Context mContext; 12 | 13 | private WeakReference mActivityWref; 14 | 15 | public static ScreenManager gDefualt; 16 | 17 | private ScreenManager(Context pContext) { 18 | this.mContext = pContext; 19 | } 20 | 21 | public static ScreenManager getInstance(Context pContext) { 22 | if (null == gDefualt) { 23 | gDefualt = new ScreenManager(pContext.getApplicationContext()); 24 | } 25 | 26 | return gDefualt; 27 | } 28 | 29 | public void setActivity(Activity pActivity) { 30 | mActivityWref = new WeakReference(pActivity); 31 | } 32 | 33 | public void startActivity(Class clazz) { 34 | Intent intent = new Intent(mContext, clazz); 35 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 36 | mContext.startActivity(intent); 37 | } 38 | 39 | public void finishActivity() { 40 | //结束掉LiveActivity 41 | if (mActivityWref != null) { 42 | Activity activity = mActivityWref.get(); 43 | if (activity != null) { 44 | activity.finish(); 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/live/WindowHelper.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.live; 2 | 3 | import android.view.Gravity; 4 | import android.view.Window; 5 | import android.view.WindowManager; 6 | 7 | public class WindowHelper { 8 | public static void set1x1Window(Window window) { 9 | //放在左上角 10 | window.setGravity(Gravity.START | Gravity.TOP); 11 | WindowManager.LayoutParams attributes = window.getAttributes(); 12 | //宽高设计为1个像素 13 | attributes.width = 1; 14 | attributes.height = 1; 15 | //起始坐标 16 | attributes.x = 0; 17 | attributes.y = 0; 18 | window.setAttributes(attributes); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/model/ActionMessage.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class ActionMessage { 11 | // 动作 12 | String action; 13 | 14 | int width = 270; 15 | int height = 480; 16 | 17 | long frequency = 1; 18 | 19 | int quality = 90; 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/model/ArkResponse.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | @Builder 12 | public class ArkResponse { 13 | public int status = 200; 14 | public String message = "ok"; 15 | 16 | public Object data; 17 | 18 | public ArkResponse(int status, String message) { 19 | this.status = status; 20 | this.message = message; 21 | } 22 | 23 | public static ArkResponse ok() { 24 | return new ArkResponse(); 25 | } 26 | 27 | public static ArkResponse bad(String msg) { 28 | return new ArkResponse(400, msg); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/util/BitmapUtil.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.util; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Base64; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | 9 | public class BitmapUtil { 10 | public static int quality = 90; 11 | 12 | /** 13 | * 将bitmap转为Base64字符串 14 | * 15 | * @param bitmap 16 | * @return base64字符串 17 | */ 18 | public static String bitmapToString(Bitmap bitmap) { 19 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 20 | 21 | bitmap.compress(Bitmap.CompressFormat.WEBP, quality, outputStream); 22 | byte[] bytes = outputStream.toByteArray(); 23 | 24 | IOUtil.closeQuitely(outputStream); 25 | 26 | return Base64.encodeToString(bytes, Base64.NO_WRAP); 27 | } 28 | 29 | /** 30 | * 将base64字符串转为bitmap 31 | * 32 | * @param base64String 33 | * @return bitmap 34 | */ 35 | public static Bitmap base64ToBitmap(String base64String) { 36 | byte[] bytes = Base64.decode(base64String, Base64.NO_WRAP); 37 | 38 | Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); 39 | return bitmap; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/util/FixedLenQueue.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.util; 2 | 3 | import android.provider.Telephony; 4 | 5 | import com.kk.screencapture.MainActivity; 6 | 7 | import java.util.LinkedList; 8 | import java.util.concurrent.ArrayBlockingQueue; 9 | import java.util.concurrent.BlockingDeque; 10 | import java.util.concurrent.ConcurrentLinkedQueue; 11 | import java.util.concurrent.locks.ReentrantLock; 12 | 13 | /** 14 | * 定长队列 15 | **/ 16 | public class FixedLenQueue { 17 | 18 | private int limit; // 队列长度 19 | 20 | public ArrayBlockingQueue queue; 21 | 22 | public FixedLenQueue(int limit) { 23 | this.limit = limit; 24 | 25 | queue = new ArrayBlockingQueue<>(limit); 26 | } 27 | 28 | /** 29 | * 入列:当队列大小已满时,把队头的元素poll掉 30 | */ 31 | public T offer(T t) { 32 | if (queue.size() == limit) { 33 | T oldestItem = queue.poll(); 34 | return oldestItem; 35 | } 36 | 37 | queue.offer(t); 38 | return null; 39 | } 40 | 41 | public T take() { 42 | try { 43 | return queue.take(); 44 | } catch (InterruptedException e) { 45 | e.printStackTrace(); 46 | } 47 | 48 | return null; 49 | } 50 | 51 | public int size() { 52 | return queue.size(); 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/util/IOUtil.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.util; 2 | 3 | import java.io.Closeable; 4 | 5 | public class IOUtil { 6 | public static void closeQuitely(Object closeable) { 7 | if (null != closeable) { 8 | try { 9 | if (closeable instanceof Closeable) 10 | ((Closeable) closeable).close(); 11 | 12 | if (closeable instanceof AutoCloseable) 13 | ((AutoCloseable) closeable).close(); 14 | } catch (Exception e) { 15 | e.printStackTrace(); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/util/MapUtil.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.util; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class MapUtil { 7 | public static long getLong0(Map map, String key) { 8 | return getLong(map, key, 0); 9 | } 10 | 11 | public static long getLong(Map map, String key, long defVal) { 12 | Object val = map.get(key); 13 | 14 | if (null == val) { 15 | return defVal; 16 | } 17 | 18 | try { 19 | return Float.valueOf(String.valueOf(val)).longValue(); 20 | } catch (Exception e) { 21 | return defVal; 22 | } 23 | } 24 | 25 | public static void main(String[] args) { 26 | Map map = new HashMap<>(); 27 | map.put("a", 1); 28 | map.put("b", 1.0f); 29 | map.put("c", "1"); 30 | map.put("d", "1.0f"); 31 | 32 | System.out.println("a:" + getLong0(map, "a")); 33 | System.out.println("b:" + getLong0(map, "b")); 34 | System.out.println("c:" + getLong0(map, "c")); 35 | System.out.println("d:" + getLong0(map, "d")); 36 | System.out.println("e:" + getLong0(map, "e")); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/kk/screencapture/util/ThreadUtil.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture.util; 2 | 3 | public class ThreadUtil { 4 | /** 5 | * 安静的睡 6 | **/ 7 | public static void sleepQuitelySecs(long secs) { 8 | sleepQuitelyMills(secs * 1000); 9 | } 10 | 11 | public static void sleepQuitelyMills(long millis) { 12 | try { 13 | Thread.sleep(millis); 14 | } catch (InterruptedException e) { 15 | e.printStackTrace(); 16 | } 17 | } 18 | 19 | /** 20 | * 运行 21 | **/ 22 | public static void doRun(Runnable runnable) { 23 | new Thread(runnable).start(); 24 | } 25 | 26 | /** 27 | * 延迟执行 28 | **/ 29 | public static void doRunDelayed(Runnable runnable, long secs) { 30 | sleepQuitelyMills(secs); 31 | 32 | doRun(runnable); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.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 | ScreenCaptureWithProjection 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/test/java/com/kk/screencapture/CaptureServerTest.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import org.junit.Test; 4 | 5 | public class CaptureServerTest { 6 | @Test 7 | public void startDaemonTest() { 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/src/test/java/com/kk/screencapture/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | Long.parseLong(null); 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/test/java/com/kk/screencapture/FixedLenQueueTest.java: -------------------------------------------------------------------------------- 1 | package com.kk.screencapture; 2 | 3 | import com.kk.screencapture.util.FixedLenQueue; 4 | import com.kk.screencapture.util.ThreadUtil; 5 | 6 | import org.junit.Test; 7 | 8 | public class FixedLenQueueTest { 9 | 10 | @Test 11 | public void basicTest() { 12 | System.out.println("test--->"+Thread.currentThread().getName()); 13 | final FixedLenQueue numsQueue = new FixedLenQueue<>(10); 14 | 15 | numsQueue.offer(1); 16 | numsQueue.offer(2); 17 | numsQueue.offer(3); 18 | 19 | new Thread(new Runnable() { 20 | @Override 21 | public void run() { 22 | System.out.println("chidl--->"+Thread.currentThread().getName()); 23 | ThreadUtil.sleepQuitelyMills(5 * 1000); 24 | numsQueue.offer(4); 25 | } 26 | }).start(); 27 | 28 | System.out.print("--->" + numsQueue.take()); 29 | System.out.print("--->" + numsQueue.take()); 30 | System.out.print("--->" + numsQueue.take()); 31 | 32 | System.out.print("--->" + numsQueue.take()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | 5 | repositories { 6 | google() 7 | jcenter() 8 | } 9 | dependencies { 10 | classpath 'com.android.tools.build:gradle:3.1.4' 11 | 12 | 13 | // NOTE: Do not place your application dependencies here; they belong 14 | // in the individual module build.gradle files 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | jcenter() 22 | } 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } 28 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri May 24 21:38:40 CST 2019 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-4.4-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.anyConstructor.suppressConstructorProperties=true 2 | android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath=true 3 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /snaps/app-ui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/snaps/app-ui.jpg -------------------------------------------------------------------------------- /snaps/snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/snaps/snap.png -------------------------------------------------------------------------------- /snaps/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/snaps/start.png -------------------------------------------------------------------------------- /snaps/usb-open.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xxhdxh/XScreencapper/c5f388299decdd1be7e7da52f4bffd8ae0815820/snaps/usb-open.jpg --------------------------------------------------------------------------------