├── .gitignore ├── .idea ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── huiger │ │ └── screenshotdemo │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── huiger │ │ │ └── screenshotdemo │ │ │ ├── BaseActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── MyDialog.java │ │ │ └── ScreenShotListenManager.java │ └── res │ │ ├── layout │ │ ├── activity_main.xml │ │ ├── dialog_layout.xml │ │ └── share_screenshot_layout.xml │ │ ├── mipmap-hdpi │ │ ├── huiger.png │ │ ├── ic_launcher.png │ │ └── timg.jpg │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values-w820dp │ │ └── dimens.xml │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── huiger │ └── screenshotdemo │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── ScreenShot.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | 47 | 48 | 49 | 50 | 1.7 51 | 52 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ScreenShotDemo 2 | 3 | Android截屏功能是一个常用的功能,可以方便的用来分享或者发送给好友,本文介绍了如何实现app内截屏监控功能,当发现用户在我们的app内进行了截屏操作时,进行对图片的二次操作,例如添加二维码,公司logo等一系列***。 4 | 5 | 6 | ## 测试截图: 7 | ![image](./img/ScreenShot.gif) 8 | 9 | ## 截屏原理 10 | Android系统并没有提供截屏通知相关的API,需要我们自己利用系统能提供的相关特性变通实现。Android系统有一个媒体数据库,每拍一张照片,或使用系统截屏截取一张图片,都会把这张图片的详细信息加入到这个媒体数据库,并发出内容改变通知,我们可以利用内容观察者(ContentObserver)监听媒体数据库的变化,当数据库有变化时,获取最后插入的一条图片数据,如果该图片符合特定的规则,则认为被截屏了。 11 | 12 | ## 判断依据 13 | 当ContentObserver监听到媒体数据库的数据改变, 在有数据改变时 获取最后插入数据库的一条图片数据, 如果符合以下规则, 则认为截屏了: 14 | 15 | 1. 时间判断,图片的生成时间在开始监听之后,并与当前时间相隔10秒内:开始监听后生成的图片才有意义,相隔10秒内说明是刚刚生成的 16 | 2. 尺寸判断,图片的尺寸没有超过屏幕的尺寸:图片尺寸超过屏幕尺寸,不可能是截屏图片 17 | 3. 路径判断,图片路径符合包含特定的关键词:这一点是关键,截屏图片的保存路径通常包含“screenshot” 18 | 19 | 这些判断是为了增加截屏检测结果的可靠性,防止误报,防止遗漏。其中截屏图片的路径正常Android系统保存的路径格式, 例如我的是:“外部存储器/storage/emulated/0/Pictures/Screenshots/Screenshot_2017-08-03-15-42-58.png”,但Android系统碎片化严重,加上其他第三方截屏APP等,所以路径关键字除了检查是否包含“screenshot”外,还可以适当增加其他关键字,详见最后的监听器完整代码。这种监听截屏的方法也不是100%准确,例如某些被root的机器使用第三方截屏APP自定义保存路径,还比如通过ADB命令在电脑上获取手机屏幕快照均不能监听到,但这也是目前可行性最高的方法,对于绝大多数用户都比较靠谱。 20 | 21 | ## 代码描述 22 | 23 | ### 监听截屏 24 | ```java 25 | public class ScreenShotListenManager { 26 | private static final String TAG = "ScreenShotListenManager"; 27 | 28 | /** 29 | * 读取媒体数据库时需要读取的列 30 | */ 31 | private static final String[] MEDIA_PROJECTIONS = { 32 | MediaStore.Images.ImageColumns.DATA, 33 | MediaStore.Images.ImageColumns.DATE_TAKEN, 34 | }; 35 | /** 36 | * 读取媒体数据库时需要读取的列, 其中 WIDTH 和 HEIGHT 字段在 API 16 以后才有 37 | */ 38 | private static final String[] MEDIA_PROJECTIONS_API_16 = { 39 | MediaStore.Images.ImageColumns.DATA, 40 | MediaStore.Images.ImageColumns.DATE_TAKEN, 41 | MediaStore.Images.ImageColumns.WIDTH, 42 | MediaStore.Images.ImageColumns.HEIGHT, 43 | }; 44 | 45 | /** 46 | * 截屏依据中的路径判断关键字 47 | */ 48 | private static final String[] KEYWORDS = { 49 | "screenshot", "screen_shot", "screen-shot", "screen shot", 50 | "screencapture", "screen_capture", "screen-capture", "screen capture", 51 | "screencap", "screen_cap", "screen-cap", "screen cap" 52 | }; 53 | 54 | private static Point sScreenRealSize; 55 | 56 | /** 57 | * 已回调过的路径 58 | */ 59 | private final static List sHasCallbackPaths = new ArrayList(); 60 | 61 | private Context mContext; 62 | 63 | private OnScreenShotListener mListener; 64 | 65 | private long mStartListenTime; 66 | 67 | /** 68 | * 内部存储器内容观察者 69 | */ 70 | private MediaContentObserver mInternalObserver; 71 | 72 | /** 73 | * 外部存储器内容观察者 74 | */ 75 | private MediaContentObserver mExternalObserver; 76 | 77 | /** 78 | * 运行在 UI 线程的 Handler, 用于运行监听器回调 79 | */ 80 | private final Handler mUiHandler = new Handler(Looper.getMainLooper()); 81 | 82 | private ScreenShotListenManager(Context context) { 83 | if (context == null) { 84 | throw new IllegalArgumentException("The context must not be null."); 85 | } 86 | mContext = context; 87 | 88 | // 获取屏幕真实的分辨率 89 | if (sScreenRealSize == null) { 90 | sScreenRealSize = getRealScreenSize(); 91 | if (sScreenRealSize != null) { 92 | Log.d(TAG, "Screen Real Size: " + sScreenRealSize.x + " * " + sScreenRealSize.y); 93 | } else { 94 | Log.w(TAG, "Get screen real size failed."); 95 | } 96 | } 97 | } 98 | 99 | public static ScreenShotListenManager newInstance(Context context) { 100 | assertInMainThread(); 101 | return new ScreenShotListenManager(context); 102 | } 103 | 104 | /** 105 | * 启动监听 106 | */ 107 | public void startListen() { 108 | assertInMainThread(); 109 | 110 | // sHasCallbackPaths.clear(); 111 | 112 | // 记录开始监听的时间戳 113 | mStartListenTime = System.currentTimeMillis(); 114 | 115 | // 创建内容观察者 116 | mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); 117 | mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); 118 | 119 | // 注册内容观察者 120 | mContext.getContentResolver().registerContentObserver( 121 | MediaStore.Images.Media.INTERNAL_CONTENT_URI, 122 | false, 123 | mInternalObserver 124 | ); 125 | mContext.getContentResolver().registerContentObserver( 126 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 127 | false, 128 | mExternalObserver 129 | ); 130 | } 131 | 132 | /** 133 | * 停止监听 134 | */ 135 | public void stopListen() { 136 | assertInMainThread(); 137 | 138 | // 注销内容观察者 139 | if (mInternalObserver != null) { 140 | try { 141 | mContext.getContentResolver().unregisterContentObserver(mInternalObserver); 142 | } catch (Exception e) { 143 | e.printStackTrace(); 144 | } 145 | mInternalObserver = null; 146 | } 147 | if (mExternalObserver != null) { 148 | try { 149 | mContext.getContentResolver().unregisterContentObserver(mExternalObserver); 150 | } catch (Exception e) { 151 | e.printStackTrace(); 152 | } 153 | mExternalObserver = null; 154 | } 155 | 156 | // 清空数据 157 | mStartListenTime = 0; 158 | // sHasCallbackPaths.clear(); 159 | 160 | //切记!!!:必须设置为空 可能mListener 会隐式持有Activity导致释放不掉 161 | mListener = null; 162 | } 163 | 164 | /** 165 | * 处理媒体数据库的内容改变 166 | */ 167 | private void handleMediaContentChange(Uri contentUri) { 168 | Cursor cursor = null; 169 | try { 170 | // 数据改变时查询数据库中最后加入的一条数据 171 | cursor = mContext.getContentResolver().query( 172 | contentUri, 173 | Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16, 174 | null, 175 | null, 176 | MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" 177 | ); 178 | 179 | if (cursor == null) { 180 | Log.e(TAG, "Deviant logic."); 181 | return; 182 | } 183 | if (!cursor.moveToFirst()) { 184 | Log.d(TAG, "Cursor no data."); 185 | return; 186 | } 187 | 188 | // 获取各列的索引 189 | int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); 190 | int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); 191 | int widthIndex = -1; 192 | int heightIndex = -1; 193 | if (Build.VERSION.SDK_INT >= 16) { 194 | widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH); 195 | heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT); 196 | } 197 | 198 | // 获取行数据 199 | String data = cursor.getString(dataIndex); 200 | long dateTaken = cursor.getLong(dateTakenIndex); 201 | int width = 0; 202 | int height = 0; 203 | if (widthIndex >= 0 && heightIndex >= 0) { 204 | width = cursor.getInt(widthIndex); 205 | height = cursor.getInt(heightIndex); 206 | } else { 207 | // API 16 之前, 宽高要手动获取 208 | Point size = getImageSize(data); 209 | width = size.x; 210 | height = size.y; 211 | } 212 | 213 | // 处理获取到的第一行数据 214 | handleMediaRowData(data, dateTaken, width, height); 215 | 216 | } catch (Exception e) { 217 | e.printStackTrace(); 218 | 219 | } finally { 220 | if (cursor != null && !cursor.isClosed()) { 221 | cursor.close(); 222 | } 223 | } 224 | } 225 | 226 | private Point getImageSize(String imagePath) { 227 | BitmapFactory.Options options = new BitmapFactory.Options(); 228 | options.inJustDecodeBounds = true; 229 | BitmapFactory.decodeFile(imagePath, options); 230 | return new Point(options.outWidth, options.outHeight); 231 | } 232 | 233 | /** 234 | * 处理获取到的一行数据 235 | */ 236 | private void handleMediaRowData(String data, long dateTaken, int width, int height) { 237 | if (checkScreenShot(data, dateTaken, width, height)) { 238 | Log.d(TAG, "ScreenShot: path = " + data + "; size = " + width + " * " + height 239 | + "; date = " + dateTaken); 240 | if (mListener != null && !checkCallback(data)) { 241 | mListener.onShot(data); 242 | } 243 | } else { 244 | // 如果在观察区间媒体数据库有数据改变,又不符合截屏规则,则输出到 log 待分析 245 | Log.w(TAG, "Media content changed, but not screenshot: path = " + data 246 | + "; size = " + width + " * " + height + "; date = " + dateTaken); 247 | } 248 | } 249 | 250 | /** 251 | * 判断指定的数据行是否符合截屏条件 252 | */ 253 | private boolean checkScreenShot(String data, long dateTaken, int width, int height) { 254 | /* 255 | * 判断依据一: 时间判断 256 | */ 257 | // 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于10秒, 则认为当前没有截屏 258 | if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { 259 | return false; 260 | } 261 | 262 | /* 263 | * 判断依据二: 尺寸判断 264 | */ 265 | if (sScreenRealSize != null) { 266 | // 如果图片尺寸超出屏幕, 则认为当前没有截屏 267 | if (!((width <= sScreenRealSize.x && height <= sScreenRealSize.y) 268 | || (height <= sScreenRealSize.x && width <= sScreenRealSize.y))) { 269 | return false; 270 | } 271 | } 272 | 273 | /* 274 | * 判断依据三: 路径判断 275 | */ 276 | if (TextUtils.isEmpty(data)) { 277 | return false; 278 | } 279 | data = data.toLowerCase(); 280 | // 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了 281 | for (String keyWork : KEYWORDS) { 282 | if (data.contains(keyWork)) { 283 | return true; 284 | } 285 | } 286 | 287 | return false; 288 | } 289 | 290 | /** 291 | * 判断是否已回调过, 某些手机ROM截屏一次会发出多次内容改变的通知;
292 | * 删除一个图片也会发通知, 同时防止删除图片时误将上一张符合截屏规则的图片当做是当前截屏. 293 | */ 294 | private boolean checkCallback(String imagePath) { 295 | if (sHasCallbackPaths.contains(imagePath)) { 296 | Log.d(TAG, "ScreenShot: imgPath has done" 297 | + "; imagePath = " + imagePath); 298 | return true; 299 | } 300 | // 大概缓存15~20条记录便可 301 | if (sHasCallbackPaths.size() >= 20) { 302 | for (int i = 0; i < 5; i++) { 303 | sHasCallbackPaths.remove(0); 304 | } 305 | } 306 | sHasCallbackPaths.add(imagePath); 307 | return false; 308 | } 309 | 310 | /** 311 | * 获取屏幕分辨率 312 | */ 313 | private Point getRealScreenSize() { 314 | Point screenSize = null; 315 | try { 316 | screenSize = new Point(); 317 | WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 318 | Display defaultDisplay = windowManager.getDefaultDisplay(); 319 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 320 | defaultDisplay.getRealSize(screenSize); 321 | } else { 322 | try { 323 | Method mGetRawW = Display.class.getMethod("getRawWidth"); 324 | Method mGetRawH = Display.class.getMethod("getRawHeight"); 325 | screenSize.set( 326 | (Integer) mGetRawW.invoke(defaultDisplay), 327 | (Integer) mGetRawH.invoke(defaultDisplay) 328 | ); 329 | } catch (Exception e) { 330 | screenSize.set(defaultDisplay.getWidth(), defaultDisplay.getHeight()); 331 | e.printStackTrace(); 332 | } 333 | } 334 | } catch (Exception e) { 335 | e.printStackTrace(); 336 | } 337 | return screenSize; 338 | } 339 | 340 | public Bitmap createScreenShotBitmap(Context context, String screenFilePath) { 341 | 342 | View v = LayoutInflater.from(context).inflate(R.layout.share_screenshot_layout, null); 343 | ImageView iv = (ImageView) v.findViewById(R.id.iv); 344 | Bitmap bitmap = BitmapFactory.decodeFile(screenFilePath); 345 | iv.setImageBitmap(bitmap); 346 | 347 | //整体布局 348 | Point point = getRealScreenSize(); 349 | v.measure(View.MeasureSpec.makeMeasureSpec(point.x, View.MeasureSpec.EXACTLY), 350 | View.MeasureSpec.makeMeasureSpec(point.y, View.MeasureSpec.EXACTLY)); 351 | 352 | v.layout(0, 0, point.x, point.y); 353 | 354 | // Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.RGB_565); 355 | Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight() + dp2px(context, 140), Bitmap.Config.ARGB_8888); 356 | Canvas c = new Canvas(result); 357 | c.drawColor(Color.WHITE); 358 | // Draw view to canvas 359 | v.draw(c); 360 | 361 | return result; 362 | } 363 | 364 | private int dp2px(Context ctx, float dp) { 365 | float scale = ctx.getResources().getDisplayMetrics().density; 366 | return (int) (dp * scale + 0.5f); 367 | } 368 | 369 | /** 370 | * 设置截屏监听器 371 | */ 372 | public void setListener(OnScreenShotListener listener) { 373 | mListener = listener; 374 | } 375 | 376 | public interface OnScreenShotListener { 377 | void onShot(String imagePath); 378 | } 379 | 380 | private static void assertInMainThread() { 381 | if (Looper.myLooper() != Looper.getMainLooper()) { 382 | StackTraceElement[] elements = Thread.currentThread().getStackTrace(); 383 | String methodMsg = null; 384 | if (elements != null && elements.length >= 4) { 385 | methodMsg = elements[3].toString(); 386 | } 387 | throw new IllegalStateException("Call the method must be in main thread: " + methodMsg); 388 | } 389 | } 390 | 391 | /** 392 | * 媒体内容观察者(观察媒体数据库的改变) 393 | */ 394 | private class MediaContentObserver extends ContentObserver { 395 | 396 | private Uri mContentUri; 397 | 398 | public MediaContentObserver(Uri contentUri, Handler handler) { 399 | super(handler); 400 | mContentUri = contentUri; 401 | } 402 | 403 | @Override 404 | public void onChange(boolean selfChange) { 405 | super.onChange(selfChange); 406 | handleMediaContentChange(mContentUri); 407 | } 408 | } 409 | 410 | 411 | } 412 | 413 | ``` 414 | 415 | ### 全局使用 416 | 我们需求是要在APP中全局都能监听截屏操作,所以,我们只需要在BaseActivity中进行监听就可以了。 417 | ```java 418 | @Override 419 | protected void onResume() { 420 | super.onResume(); 421 | startScreenShotListen(); 422 | } 423 | 424 | @Override 425 | protected void onPause() { 426 | super.onPause(); 427 | stopScreenShotListen(); 428 | } 429 | 430 | /** 431 | * 监听 432 | */ 433 | private void startScreenShotListen() { 434 | if (!isHasScreenShotListener && screenShotListenManager != null) { 435 | screenShotListenManager.setListener(new ScreenShotListenManager.OnScreenShotListener() { 436 | @Override 437 | public void onShot(String imagePath) { 438 | 439 | path = imagePath; 440 | Log.d("msg", "BaseActivity -> onShot: " + "获得截图路径:" + imagePath); 441 | 442 | MyDialog ksDialog = MyDialog.getInstance() 443 | .init(BaseActivity.this, R.layout.dialog_layout) 444 | .setCancelButton("取消", null) 445 | .setPositiveButton("查看", new MyDialog.OnClickListener() { 446 | @Override 447 | public void OnClick(View view) { 448 | Bitmap screenShotBitmap = screenShotListenManager.createScreenShotBitmap(mContext, path); 449 | 450 | // 此处只要分享这个合成的Bitmap图片就行了 451 | // 为了演示,故写下面代码 452 | screenShotIv.setImageBitmap(screenShotBitmap); 453 | } 454 | }); 455 | 456 | screenShotIv = (ImageView) ksDialog.getView(R.id.iv); 457 | progressBar = (ProgressBar) ksDialog.getView(R.id.avLoad); 458 | mHandler.postDelayed(new Runnable() { 459 | @Override 460 | public void run() { 461 | progressBar.setVisibility(View.GONE); 462 | Glide.with(mContext).load(path).into(screenShotIv); 463 | 464 | } 465 | }, 1500); 466 | } 467 | }); 468 | screenShotListenManager.startListen(); 469 | isHasScreenShotListener = true; 470 | } 471 | } 472 | 473 | /** 474 | * 停止监听 475 | */ 476 | private void stopScreenShotListen() { 477 | if (isHasScreenShotListener && screenShotListenManager != null) { 478 | screenShotListenManager.stopListen(); 479 | isHasScreenShotListener = false; 480 | } 481 | } 482 | ``` 483 | 484 | 至此APP内监听截屏操作就完成了,我们需要在baseActivity中执行监听并执行相应操作,不需要写更多代码。 485 | 486 | 487 | - [参考来源](https://xiets.blog.csdn.net/article/details/52692163) 488 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 30 5 | buildToolsVersion "30.0.0" 6 | defaultConfig { 7 | applicationId "com.huiger.screenshotdemo" 8 | minSdkVersion 15 9 | targetSdkVersion 30 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(include: ['*.jar'], dir: 'libs') 24 | androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { 25 | exclude group: 'com.android.support', module: 'support-annotations' 26 | }) 27 | compile 'com.android.support:appcompat-v7:24.2.1' 28 | testCompile 'junit:junit:4.12' 29 | compile 'com.github.bumptech.glide:glide:4.0.0' 30 | } 31 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/huiger/screenshotdemo/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.huiger.screenshotdemo; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.huiger.screenshotdemo", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/java/com/huiger/screenshotdemo/BaseActivity.java: -------------------------------------------------------------------------------- 1 | package com.huiger.screenshotdemo; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.support.annotation.Nullable; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.util.Log; 10 | import android.view.View; 11 | import android.widget.ImageView; 12 | import android.widget.ProgressBar; 13 | 14 | import com.bumptech.glide.Glide; 15 | 16 | /** 17 | * Created by on 2017/8/3. 18 | */ 19 | 20 | public abstract class BaseActivity extends AppCompatActivity { 21 | 22 | private Context mContext; 23 | private ScreenShotListenManager screenShotListenManager; 24 | private boolean isHasScreenShotListener = false; 25 | private String path; 26 | private ImageView screenShotIv; 27 | private ProgressBar progressBar; 28 | private Handler mHandler = new Handler(); 29 | 30 | @Override 31 | protected void onCreate(@Nullable Bundle savedInstanceState) { 32 | super.onCreate(savedInstanceState); 33 | setContentView(setViewId()); 34 | screenShotListenManager = ScreenShotListenManager.newInstance(this); 35 | mContext = this; 36 | } 37 | 38 | 39 | @Override 40 | protected void onResume() { 41 | super.onResume(); 42 | startScreenShotListen(); 43 | } 44 | 45 | @Override 46 | protected void onPause() { 47 | super.onPause(); 48 | stopScreenShotListen(); 49 | } 50 | 51 | /** 52 | * 监听 53 | */ 54 | private void startScreenShotListen() { 55 | if (!isHasScreenShotListener && screenShotListenManager != null) { 56 | screenShotListenManager.setListener(new ScreenShotListenManager.OnScreenShotListener() { 57 | @Override 58 | public void onShot(String imagePath) { 59 | 60 | path = imagePath; 61 | Log.d("msg", "BaseActivity -> onShot: " + "获得截图路径:" + imagePath); 62 | 63 | MyDialog ksDialog = MyDialog.getInstance() 64 | .init(BaseActivity.this, R.layout.dialog_layout) 65 | .setCancelButton("取消", null) 66 | .setPositiveButton("生成新图片", new MyDialog.OnClickListener() { 67 | @Override 68 | public void OnClick(View view) { 69 | Bitmap screenShotBitmap = screenShotListenManager.createScreenShotBitmap(mContext, path); 70 | 71 | // 此处只要分享这个合成的Bitmap图片就行了 72 | // 为了演示,故写下面代码 73 | screenShotIv.setImageBitmap(screenShotBitmap); 74 | } 75 | }); 76 | 77 | screenShotIv = (ImageView) ksDialog.getView(R.id.iv); 78 | progressBar = (ProgressBar) ksDialog.getView(R.id.avLoad); 79 | mHandler.postDelayed(new Runnable() { 80 | @Override 81 | public void run() { 82 | progressBar.setVisibility(View.GONE); 83 | Glide.with(mContext).load(path).into(screenShotIv); 84 | 85 | } 86 | }, 1500); 87 | } 88 | }); 89 | screenShotListenManager.startListen(); 90 | isHasScreenShotListener = true; 91 | } 92 | } 93 | 94 | /** 95 | * 停止监听 96 | */ 97 | private void stopScreenShotListen() { 98 | if (isHasScreenShotListener && screenShotListenManager != null) { 99 | screenShotListenManager.stopListen(); 100 | isHasScreenShotListener = false; 101 | } 102 | } 103 | 104 | protected abstract int setViewId(); 105 | 106 | } 107 | -------------------------------------------------------------------------------- /app/src/main/java/com/huiger/screenshotdemo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.huiger.screenshotdemo; 2 | 3 | public class MainActivity extends BaseActivity { 4 | 5 | @Override 6 | protected int setViewId() { 7 | return R.layout.activity_main; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/src/main/java/com/huiger/screenshotdemo/MyDialog.java: -------------------------------------------------------------------------------- 1 | package com.huiger.screenshotdemo; 2 | 3 | import android.app.Dialog; 4 | import android.content.Context; 5 | import android.support.annotation.IdRes; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.v7.app.AlertDialog; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.widget.Button; 11 | 12 | /** 13 | * Created by on 2017/8/3. 14 | */ 15 | 16 | public class MyDialog { 17 | private Dialog dialog; 18 | private Context context; 19 | private static MyDialog MyDialog = null; 20 | private View rootView; 21 | 22 | public static MyDialog getInstance() { 23 | if (MyDialog == null) { 24 | synchronized (MyDialog.class) { 25 | if (MyDialog == null) { 26 | MyDialog = new MyDialog(); 27 | } 28 | } 29 | } 30 | 31 | return MyDialog; 32 | } 33 | 34 | private MyDialog() { 35 | } 36 | 37 | 38 | public MyDialog init(Context context, @LayoutRes int resId){ 39 | this.context = context; 40 | rootView = LayoutInflater.from(context).inflate(resId, null); 41 | dialog = new AlertDialog.Builder(context).create(); 42 | dialog.show(); 43 | dialog.setContentView(rootView); 44 | return this; 45 | } 46 | 47 | 48 | public MyDialog setPositiveButton(String str, final OnClickListener listener) { 49 | Button button = (Button) rootView.findViewById(R.id.btn1); 50 | button.setText(str); 51 | button.setOnClickListener(new View.OnClickListener() { 52 | @Override 53 | public void onClick(View v) { 54 | if (listener == null) { 55 | dialog.dismiss(); 56 | } else { 57 | listener.OnClick(v); 58 | // dialog.dismiss(); 59 | } 60 | } 61 | }); 62 | return this; 63 | } 64 | 65 | public MyDialog setCancelButton(String str, final OnClickListener listener) { 66 | Button button = (Button) rootView.findViewById(R.id.btn2); 67 | button.setText(str); 68 | button.setOnClickListener(new View.OnClickListener() { 69 | @Override 70 | public void onClick(View v) { 71 | if (listener == null) { 72 | dialog.dismiss(); 73 | } else { 74 | listener.OnClick(v); 75 | dialog.dismiss(); 76 | } 77 | } 78 | }); 79 | return this; 80 | } 81 | 82 | 83 | 84 | public View getView(@IdRes int resId){ 85 | return rootView.findViewById(resId); 86 | } 87 | 88 | public interface OnClickListener { 89 | void OnClick(View view); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /app/src/main/java/com/huiger/screenshotdemo/ScreenShotListenManager.java: -------------------------------------------------------------------------------- 1 | package com.huiger.screenshotdemo; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.Context; 5 | import android.database.ContentObserver; 6 | import android.database.Cursor; 7 | import android.graphics.Bitmap; 8 | import android.graphics.BitmapFactory; 9 | import android.graphics.Canvas; 10 | import android.graphics.Color; 11 | import android.graphics.Point; 12 | import android.net.Uri; 13 | import android.os.Build; 14 | import android.os.Bundle; 15 | import android.os.Handler; 16 | import android.os.Looper; 17 | import android.provider.MediaStore; 18 | import android.text.TextUtils; 19 | import android.util.Log; 20 | import android.view.Display; 21 | import android.view.LayoutInflater; 22 | import android.view.View; 23 | import android.view.WindowManager; 24 | import android.widget.ImageView; 25 | 26 | import java.lang.reflect.Method; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | 31 | /** 32 | * Created by on 2017/8/2. 33 | */ 34 | 35 | public class ScreenShotListenManager { 36 | private static final String TAG = "ScreenShotListenManager"; 37 | 38 | /** 39 | * 读取媒体数据库时需要读取的列 40 | */ 41 | private static final String[] MEDIA_PROJECTIONS = { 42 | MediaStore.Images.ImageColumns.DATA, 43 | MediaStore.Images.ImageColumns.DATE_TAKEN, 44 | }; 45 | /** 46 | * 读取媒体数据库时需要读取的列, 其中 WIDTH 和 HEIGHT 字段在 API 16 以后才有 47 | */ 48 | private static final String[] MEDIA_PROJECTIONS_API_16 = { 49 | MediaStore.Images.ImageColumns.DATA, 50 | MediaStore.Images.ImageColumns.DATE_TAKEN, 51 | MediaStore.Images.ImageColumns.WIDTH, 52 | MediaStore.Images.ImageColumns.HEIGHT, 53 | }; 54 | 55 | /** 56 | * 截屏依据中的路径判断关键字 57 | */ 58 | private static final String[] KEYWORDS = { 59 | "screenshot", "screen_shot", "screen-shot", "screen shot", 60 | "screencapture", "screen_capture", "screen-capture", "screen capture", 61 | "screencap", "screen_cap", "screen-cap", "screen cap" 62 | }; 63 | 64 | private static Point sScreenRealSize; 65 | 66 | /** 67 | * 已回调过的路径 68 | */ 69 | private final static List sHasCallbackPaths = new ArrayList(); 70 | 71 | private Context mContext; 72 | 73 | private OnScreenShotListener mListener; 74 | 75 | private long mStartListenTime; 76 | 77 | /** 78 | * 内部存储器内容观察者 79 | */ 80 | private MediaContentObserver mInternalObserver; 81 | 82 | /** 83 | * 外部存储器内容观察者 84 | */ 85 | private MediaContentObserver mExternalObserver; 86 | 87 | /** 88 | * 运行在 UI 线程的 Handler, 用于运行监听器回调 89 | */ 90 | private final Handler mUiHandler = new Handler(Looper.getMainLooper()); 91 | 92 | private ScreenShotListenManager(Context context) { 93 | if (context == null) { 94 | throw new IllegalArgumentException("The context must not be null."); 95 | } 96 | mContext = context; 97 | 98 | // 获取屏幕真实的分辨率 99 | if (sScreenRealSize == null) { 100 | sScreenRealSize = getRealScreenSize(); 101 | if (sScreenRealSize != null) { 102 | Log.d(TAG, "Screen Real Size: " + sScreenRealSize.x + " * " + sScreenRealSize.y); 103 | } else { 104 | Log.w(TAG, "Get screen real size failed."); 105 | } 106 | } 107 | } 108 | 109 | public static ScreenShotListenManager newInstance(Context context) { 110 | assertInMainThread(); 111 | return new ScreenShotListenManager(context); 112 | } 113 | 114 | /** 115 | * 启动监听 116 | */ 117 | public void startListen() { 118 | assertInMainThread(); 119 | 120 | // sHasCallbackPaths.clear(); 121 | 122 | // 记录开始监听的时间戳 123 | mStartListenTime = System.currentTimeMillis(); 124 | 125 | // 创建内容观察者 126 | mInternalObserver = new MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler); 127 | mExternalObserver = new MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler); 128 | 129 | // 注册内容观察者 130 | if (Build.VERSION.SDK_INT < 29) { //Android 9及以下版本,否则不会回调onChange() 131 | // 注册内容观察者 132 | mContext.getContentResolver().registerContentObserver( 133 | MediaStore.Images.Media.INTERNAL_CONTENT_URI, 134 | false, 135 | mInternalObserver 136 | ); 137 | mContext.getContentResolver().registerContentObserver( 138 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 139 | false, 140 | mExternalObserver 141 | ); 142 | } else { //Android 10,11以上版本 143 | // 注册内容观察者 144 | mContext.getContentResolver().registerContentObserver( 145 | MediaStore.Images.Media.INTERNAL_CONTENT_URI, 146 | true, 147 | mInternalObserver 148 | ); 149 | mContext.getContentResolver().registerContentObserver( 150 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 151 | true, 152 | mExternalObserver 153 | ); 154 | } 155 | } 156 | 157 | /** 158 | * 停止监听 159 | */ 160 | public void stopListen() { 161 | assertInMainThread(); 162 | 163 | // 注销内容观察者 164 | if (mInternalObserver != null) { 165 | try { 166 | mContext.getContentResolver().unregisterContentObserver(mInternalObserver); 167 | } catch (Exception e) { 168 | e.printStackTrace(); 169 | } 170 | mInternalObserver = null; 171 | } 172 | if (mExternalObserver != null) { 173 | try { 174 | mContext.getContentResolver().unregisterContentObserver(mExternalObserver); 175 | } catch (Exception e) { 176 | e.printStackTrace(); 177 | } 178 | mExternalObserver = null; 179 | } 180 | 181 | // 清空数据 182 | mStartListenTime = 0; 183 | // sHasCallbackPaths.clear(); 184 | 185 | //切记!!!:必须设置为空 可能mListener 会隐式持有Activity导致释放不掉 186 | mListener = null; 187 | } 188 | 189 | /** 190 | * 处理媒体数据库的内容改变 191 | */ 192 | private void handleMediaContentChange(Uri contentUri) { 193 | Cursor cursor = null; 194 | try { 195 | // 数据改变时查询数据库中最后加入的一条数据 196 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 197 | Bundle bundle = new Bundle(); 198 | bundle.putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, new String[]{MediaStore.Images.Media.DATE_ADDED}); 199 | bundle.putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING); 200 | bundle.putInt(ContentResolver.QUERY_ARG_LIMIT, 1); 201 | 202 | cursor = mContext.getContentResolver().query( 203 | contentUri, 204 | Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16, 205 | bundle, null 206 | ); 207 | } else { 208 | cursor = mContext.getContentResolver().query( 209 | contentUri, 210 | Build.VERSION.SDK_INT < 16 ? MEDIA_PROJECTIONS : MEDIA_PROJECTIONS_API_16, 211 | null, 212 | null, 213 | MediaStore.Images.ImageColumns.DATE_ADDED + " desc limit 1" 214 | ); 215 | } 216 | 217 | if (cursor == null) { 218 | Log.e(TAG, "Deviant logic."); 219 | return; 220 | } 221 | if (!cursor.moveToFirst()) { 222 | Log.d(TAG, "Cursor no data."); 223 | return; 224 | } 225 | 226 | // 获取各列的索引 227 | int dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); 228 | int dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN); 229 | int widthIndex = -1; 230 | int heightIndex = -1; 231 | if (Build.VERSION.SDK_INT >= 16) { 232 | widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH); 233 | heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT); 234 | } 235 | 236 | // 获取行数据 237 | String data = cursor.getString(dataIndex); 238 | long dateTaken = cursor.getLong(dateTakenIndex); 239 | int width = 0; 240 | int height = 0; 241 | if (widthIndex >= 0 && heightIndex >= 0) { 242 | width = cursor.getInt(widthIndex); 243 | height = cursor.getInt(heightIndex); 244 | } else { 245 | // API 16 之前, 宽高要手动获取 246 | Point size = getImageSize(data); 247 | width = size.x; 248 | height = size.y; 249 | } 250 | 251 | // 处理获取到的第一行数据 252 | handleMediaRowData(data, dateTaken, width, height); 253 | 254 | } catch (Exception e) { 255 | e.printStackTrace(); 256 | 257 | } finally { 258 | if (cursor != null && !cursor.isClosed()) { 259 | cursor.close(); 260 | } 261 | } 262 | } 263 | 264 | private Point getImageSize(String imagePath) { 265 | BitmapFactory.Options options = new BitmapFactory.Options(); 266 | options.inJustDecodeBounds = true; 267 | BitmapFactory.decodeFile(imagePath, options); 268 | return new Point(options.outWidth, options.outHeight); 269 | } 270 | 271 | /** 272 | * 处理获取到的一行数据 273 | */ 274 | private void handleMediaRowData(String data, long dateTaken, int width, int height) { 275 | if (checkScreenShot(data, dateTaken, width, height)) { 276 | Log.d(TAG, "ScreenShot: path = " + data + "; size = " + width + " * " + height 277 | + "; date = " + dateTaken); 278 | if (mListener != null && !checkCallback(data)) { 279 | mListener.onShot(data); 280 | } 281 | } else { 282 | // 如果在观察区间媒体数据库有数据改变,又不符合截屏规则,则输出到 log 待分析 283 | Log.w(TAG, "Media content changed, but not screenshot: path = " + data 284 | + "; size = " + width + " * " + height + "; date = " + dateTaken); 285 | } 286 | } 287 | 288 | /** 289 | * 判断指定的数据行是否符合截屏条件 290 | */ 291 | private boolean checkScreenShot(String data, long dateTaken, int width, int height) { 292 | /* 293 | * 判断依据一: 时间判断 294 | */ 295 | // 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于10秒, 则认为当前没有截屏 296 | if (dateTaken < mStartListenTime || (System.currentTimeMillis() - dateTaken) > 10 * 1000) { 297 | return false; 298 | } 299 | 300 | /* 301 | * 判断依据二: 尺寸判断 302 | */ 303 | if (sScreenRealSize != null) { 304 | // 如果图片尺寸超出屏幕, 则认为当前没有截屏 305 | if (!((width <= sScreenRealSize.x && height <= sScreenRealSize.y) 306 | || (height <= sScreenRealSize.x && width <= sScreenRealSize.y))) { 307 | return false; 308 | } 309 | } 310 | 311 | /* 312 | * 判断依据三: 路径判断 313 | */ 314 | if (TextUtils.isEmpty(data)) { 315 | return false; 316 | } 317 | data = data.toLowerCase(); 318 | // 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了 319 | for (String keyWork : KEYWORDS) { 320 | if (data.contains(keyWork)) { 321 | return true; 322 | } 323 | } 324 | 325 | return false; 326 | } 327 | 328 | /** 329 | * 判断是否已回调过, 某些手机ROM截屏一次会发出多次内容改变的通知;
330 | * 删除一个图片也会发通知, 同时防止删除图片时误将上一张符合截屏规则的图片当做是当前截屏. 331 | */ 332 | private boolean checkCallback(String imagePath) { 333 | if (sHasCallbackPaths.contains(imagePath)) { 334 | Log.d(TAG, "ScreenShot: imgPath has done" 335 | + "; imagePath = " + imagePath); 336 | return true; 337 | } 338 | // 大概缓存15~20条记录便可 339 | if (sHasCallbackPaths.size() >= 20) { 340 | for (int i = 0; i < 5; i++) { 341 | sHasCallbackPaths.remove(0); 342 | } 343 | } 344 | sHasCallbackPaths.add(imagePath); 345 | return false; 346 | } 347 | 348 | /** 349 | * 获取屏幕分辨率 350 | */ 351 | private Point getRealScreenSize() { 352 | Point screenSize = null; 353 | try { 354 | screenSize = new Point(); 355 | WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE); 356 | Display defaultDisplay = windowManager.getDefaultDisplay(); 357 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { 358 | defaultDisplay.getRealSize(screenSize); 359 | } else { 360 | try { 361 | Method mGetRawW = Display.class.getMethod("getRawWidth"); 362 | Method mGetRawH = Display.class.getMethod("getRawHeight"); 363 | screenSize.set( 364 | (Integer) mGetRawW.invoke(defaultDisplay), 365 | (Integer) mGetRawH.invoke(defaultDisplay) 366 | ); 367 | } catch (Exception e) { 368 | screenSize.set(defaultDisplay.getWidth(), defaultDisplay.getHeight()); 369 | e.printStackTrace(); 370 | } 371 | } 372 | } catch (Exception e) { 373 | e.printStackTrace(); 374 | } 375 | return screenSize; 376 | } 377 | 378 | public Bitmap createScreenShotBitmap(Context context, String screenFilePath) { 379 | 380 | View v = LayoutInflater.from(context).inflate(R.layout.share_screenshot_layout, null); 381 | ImageView iv = (ImageView) v.findViewById(R.id.iv); 382 | Bitmap bitmap = BitmapFactory.decodeFile(screenFilePath); 383 | iv.setImageBitmap(bitmap); 384 | 385 | //整体布局 386 | Point point = getRealScreenSize(); 387 | v.measure(View.MeasureSpec.makeMeasureSpec(point.x, View.MeasureSpec.EXACTLY), 388 | View.MeasureSpec.makeMeasureSpec(point.y, View.MeasureSpec.EXACTLY)); 389 | 390 | v.layout(0, 0, point.x, point.y); 391 | 392 | // Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.RGB_565); 393 | Bitmap result = Bitmap.createBitmap(v.getWidth(), v.getHeight() + dp2px(context, 140), Bitmap.Config.ARGB_8888); 394 | Canvas c = new Canvas(result); 395 | c.drawColor(Color.WHITE); 396 | // Draw view to canvas 397 | v.draw(c); 398 | 399 | return result; 400 | } 401 | 402 | private int dp2px(Context ctx, float dp) { 403 | float scale = ctx.getResources().getDisplayMetrics().density; 404 | return (int) (dp * scale + 0.5f); 405 | } 406 | 407 | /** 408 | * 设置截屏监听器 409 | */ 410 | public void setListener(OnScreenShotListener listener) { 411 | mListener = listener; 412 | } 413 | 414 | public interface OnScreenShotListener { 415 | void onShot(String imagePath); 416 | } 417 | 418 | private static void assertInMainThread() { 419 | if (Looper.myLooper() != Looper.getMainLooper()) { 420 | StackTraceElement[] elements = Thread.currentThread().getStackTrace(); 421 | String methodMsg = null; 422 | if (elements != null && elements.length >= 4) { 423 | methodMsg = elements[3].toString(); 424 | } 425 | throw new IllegalStateException("Call the method must be in main thread: " + methodMsg); 426 | } 427 | } 428 | 429 | /** 430 | * 媒体内容观察者(观察媒体数据库的改变) 431 | */ 432 | private class MediaContentObserver extends ContentObserver { 433 | 434 | private Uri mContentUri; 435 | 436 | public MediaContentObserver(Uri contentUri, Handler handler) { 437 | super(handler); 438 | mContentUri = contentUri; 439 | } 440 | 441 | @Override 442 | public void onChange(boolean selfChange) { 443 | super.onChange(selfChange); 444 | handleMediaContentChange(mContentUri); 445 | } 446 | } 447 | 448 | 449 | } 450 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 14 | 15 | 20 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 41 | 42 |