├── .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 | 
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 | 
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 | 
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 | 
73 |
74 | #### 截屏页
75 |
76 | 
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------