├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── android
│ │ └── screenshot
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── android
│ │ │ └── screenshot
│ │ │ ├── activity
│ │ │ ├── BaseActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── PermissionsActivity.kt
│ │ │ ├── adapter
│ │ │ ├── FunctionAdapter.kt
│ │ │ ├── FunctionButton.kt
│ │ │ └── FunctionViewHolder.kt
│ │ │ ├── functions
│ │ │ ├── AbsFunction.kt
│ │ │ ├── IFunction.kt
│ │ │ ├── ScreenShotEndFunction.kt
│ │ │ └── ScreenShotStartFunction.kt
│ │ │ ├── screenshot
│ │ │ ├── ScreenShotEventDispatcher.kt
│ │ │ ├── ScreenShotListener.kt
│ │ │ ├── ScreenShotMonitor.kt
│ │ │ └── ScreenShotPreviewView.kt
│ │ │ └── utils
│ │ │ ├── AppMonitor.kt
│ │ │ ├── Extension.kt
│ │ │ ├── Logger.kt
│ │ │ ├── PermissionsUtils.kt
│ │ │ └── Weak.kt
│ └── res
│ │ ├── drawable
│ │ ├── function_item.xml
│ │ └── icon.png
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── view_function_item.xml
│ │ └── view_screenshot_preview.xml
│ │ ├── values-night
│ │ └── themes.xml
│ │ ├── values-v23
│ │ └── themes.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ ├── styles.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── android
│ └── screenshot
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 前瞻
2 | 目前Android针对截屏的监控主要有三种方式:
3 |
4 | 1. 利用FileObserver监听某个目录中资源的变化
5 | 2. 利用ContentObserver监听全部资源的变化
6 | 3. 直接监听截屏快捷键(由于不同的厂商自定义的原因,使用这种方法进行监听比较困难)
7 |
8 | 本文主要使用ContentObserver的方式来实现对截屏的监控。
9 |
10 | # Android 各版本适配
11 | 主要针对Android 13及Android 14更新的存储权限进行适配。
12 |
13 | 在Android 13中,存储权限从原来的`READ_EXTERNAL_STORAGE`细化成为`READ_MEDIA_IMAGES`/`READ_MEDIA_VIDEO`/`READ_MEDIA_AUDIO`三种权限,在进行权限判断的时候需要进行版本区分。
14 |
15 | 在Android 14中,存储权限从Android 13的细化权限中更新成为允许用户选择部分图片资源给应用访问。但是针对截屏增加了一个新的截屏监控权限`DETECT_SCREEN_CAPTURE`,该权限默认为开且用户无感知,针对用户只给部分权限的情况,我们可以通过该权限来获取用户的截屏动作,尝试一些不依赖截屏文件的操作。
16 |
17 | |权限状态|Android 13及以下机型|Android 14及以上机型|
18 | |----|----|---|
19 | |有全部相册权限|使用媒体库监控实现监控|使用媒体库监控实现监控
20 | |有部分相册权限|无法进行监控|使用系统API进行监控(但无法拿到截屏文件)
21 | |没有相册权限|无法进行监控|使用系统API进行监控(但无法拿到截屏文件)
22 |
23 | ## Android 13及以下机型监控
24 | > 针对Android 13及以下用户,使用监听媒体库方式进行截屏的监控
25 | ### 1. 建立相关截屏媒体库,分别监控内部存储及外部存储
26 | ```kotlin
27 | private inner class MediaContentObserver(private val contentUri: Uri, handler: Handler?) : ContentObserver(handler) {
28 | override fun onChange(selfChange: Boolean, uri: Uri?) {
29 | // handle screenshot file
30 | }
31 | }
32 | ```
33 | 在合适时机,通过`registerActivityLifecycleCallbacks`的方法将截屏的开始监控及取消监控注入到每个activity的生命周期中。将开始监控媒体库方法注入每个activity的`onResume`中,将停止监控注入每个activity的`onPause`中,保证activity在展示的时候开始监控截屏,在消失的时候结束对截屏的监控。
34 | ```kotlin
35 | application.registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
36 | override fun onActivityResumed(activity: Activity) {
37 | CoroutineScope(Dispatchers.IO).launch {
38 | startListen(WeakReference(activity))
39 | }
40 | }
41 |
42 | override fun onActivityPaused(activity: Activity) {
43 | CoroutineScope(Dispatchers.IO).launch {
44 | stopListen(WeakReference(activity))
45 | }
46 | }
47 |
48 | override fun onActivityStarted(activity: Activity) {}
49 |
50 | override fun onActivityStopped(activity: Activity) {}
51 |
52 | override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
53 |
54 | override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
55 |
56 | override fun onActivityDestroyed(activity: Activity) {}
57 |
58 | })
59 | ```
60 | 如果不希望这样实现,也可以直接将相关能力注入到需要被监控activity的生命周期中,而不是所有的activity。
61 |
62 | 在对应的生命周期中实现对媒体库的绑定与解绑。
63 | ```kotlin
64 | private fun registerObserver(activity: Activity) {
65 | internalObserver = MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, uiHandler)
66 | externalObserver = MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, uiHandler)
67 | startListenTime = System.currentTimeMillis()
68 | internalObserver?.let {
69 | activity.applicationContext.contentResolver.registerContentObserver(
70 | MediaStore.Images.Media.INTERNAL_CONTENT_URI,
71 | Build.VERSION.SDK_INT > Build.VERSION_CODES.P, it,
72 | )
73 | }
74 | externalObserver?.let {
75 | activity.applicationContext.contentResolver.registerContentObserver(
76 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
77 | Build.VERSION.SDK_INT > Build.VERSION_CODES.P, it,
78 | )
79 | }
80 | }
81 |
82 | private fun unregisterObserver(activity: Activity) {
83 | try {
84 | internalObserver?.let {
85 | activity.contentResolver.unregisterContentObserver(it)
86 | }
87 | } catch (_: Exception) {}
88 | internalObserver = null
89 |
90 | try {
91 | externalObserver?.let {
92 | activity.contentResolver.unregisterContentObserver(it)
93 | }
94 | } catch (_: Exception) {}
95 | externalObserver = null
96 | startListenTime = 0
97 | }
98 | ```
99 | ### 2. 监听到媒体库变化后,获取最新的文件并判断是否是截屏文件
100 | #### 2.1 获取最新媒体库文件
101 | 获取最新文件主要通过contentResolver通过DATE_MODIFIED来倒序获取第一个
102 | ```kotlin
103 | private fun getContentResolverCursor(
104 | contentUri: Uri,
105 | context: Context,
106 | maxCount: Int = 1
107 | ) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
108 | val bundle = Bundle().apply {
109 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(MediaStore.Images.ImageColumns.DATE_MODIFIED))
110 | putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
111 | putInt(ContentResolver.QUERY_ARG_LIMIT, maxCount)
112 | }
113 | context.contentResolver.query(
114 | contentUri,
115 | MEDIA_PROJECTIONS_API_16,
116 | bundle,
117 | null,
118 | )
119 | } else {
120 | context.contentResolver.query(
121 | contentUri,
122 | MEDIA_PROJECTIONS,
123 | null,
124 | null,
125 | "${MediaStore.Images.ImageColumns.DATE_MODIFIED} desc limit ${maxCount}",
126 | )
127 | }
128 | ```
129 | 其中,针对不同版本的Android机型,获取的字段也做了相应的处理
130 | - Android 10及以上
131 | ``` kotlin
132 | val MEDIA_PROJECTIONS_API_16 = arrayOf(
133 | MediaStore.Images.ImageColumns.DATA,
134 | MediaStore.Images.ImageColumns.DATE_ADDED,
135 | MediaStore.Images.ImageColumns.WIDTH,
136 | MediaStore.Images.ImageColumns.HEIGHT,
137 | )
138 | ```
139 | - Android 10以下
140 | ``` kotlin
141 | val MEDIA_PROJECTIONS = arrayOf(
142 | MediaStore.Images.ImageColumns.DATA,
143 | MediaStore.Images.ImageColumns.DATE_ADDED,
144 | )
145 | ```
146 | #### 2.2 判断是否为截屏文件
147 | 判断是否为截屏文件主要通过以下三个维度来进行判断
148 | - 路径维度
149 | - 时间维度
150 | - 尺寸维度
151 | ##### 路径维度
152 | 判断获取到的文件路径是否包含screenshot相关字段
153 | ``` kotlin
154 | private fun isFilePathLegal(filePath: String?): Boolean {
155 | // File path is not empty
156 | if (filePath == null || TextUtils.isEmpty(filePath)) {
157 | return false
158 | }
159 | // File path contains screenshot KEYWORDS
160 | var hasValidScreenShot = false
161 | val lowerPath = filePath.lowercase(Locale.getDefault())
162 | for (keyWork: String in KEYWORDS) {
163 | if (lowerPath.contains(keyWork)) {
164 | hasValidScreenShot = true
165 | break
166 | }
167 | }
168 | ```
169 | 其中的关键字包括:
170 | ```kotlin
171 | private val KEYWORDS = arrayOf(
172 | "screenshot", "screen_shot", "screen-shot", "screen shot",
173 | "screencapture", "screen_capture", "screen-capture", "screen capture",
174 | "screencap", "screen_cap", "screen-cap", "screen cap",
175 | )
176 | ```
177 | ##### 时间维度
178 | > 判断文件创建的时间是否晚于开始监听截屏的时间同时文件创建的时间和当前时间相差小于10s
179 | ```kotlin
180 | private fun isFileCreationTimeLegal(dateAdded: Long?, startListenTime: Long?) =
181 | if (dateAdded == null
182 | || startListenTime == null
183 | || dateAdded / 1000 < startListenTime / 1000
184 | || (System.currentTimeMillis() - dateAdded) > MAX_COST_TIME
185 | ) false else true
186 | ```
187 | ##### 尺寸维度
188 | > 判断获取图片的大小和手机尺寸的大小是否一致
189 | ```kotlin
190 | private fun isFileSizeLegal(width: Int?, height: Int?) =
191 | screenRealSize?.let {
192 | if (width == null || height == null) {
193 | false
194 | } else if (!((width <= it.x && height <= it.y) || (height <= it.x && width <= it.y))) {
195 | false
196 | } else {
197 | true
198 | }
199 | } ?: false
200 | ```
201 | 下面是获取屏幕尺寸的方法
202 | ```kotlin
203 | private fun getRealScreenSize(context: Context): Point? {
204 | var screenSize: Point? = null
205 | try {
206 | screenSize = Point()
207 | val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
208 | val defaultDisplay = windowManager.defaultDisplay
209 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
210 | defaultDisplay.getRealSize(screenSize)
211 | } else {
212 | try {
213 | val rawWidth = Display::class.java.getMethod("getRawWidth")
214 | val rawHeight = Display::class.java.getMethod("getRawHeight")
215 | screenSize.set(
216 | (rawWidth.invoke(defaultDisplay) as Int),
217 | (rawHeight.invoke(defaultDisplay) as Int),
218 | )
219 | } catch (_: Exception) {
220 | screenSize.set(defaultDisplay.width, defaultDisplay.height)
221 | }
222 | }
223 | } catch (_: Exception) { }
224 | return screenSize
225 | }
226 | ```
227 | ### 3. 处理截屏文件
228 | 当判断为是截屏文件后,对截屏文件进行处理,这里通过一个全局变量的listener来控制监听到截屏后的动作,针对不同的场景对listener做动态的更新。
229 | 完整的截屏文件判断流程:
230 | ```kotlin
231 | fun handleMediaContentChange(
232 | contentUri: Uri,
233 | context: Context?,
234 | startListenTime: Long?
235 | ) {
236 | CoroutineScope(Dispatchers.IO).launch {
237 | if (context == null) return@launch
238 | if (screenRealSize == null) screenRealSize = getRealScreenSize(context)
239 |
240 | var cursor: Cursor? = null
241 |
242 | try {
243 | cursor = getContentResolverCursor(contentUri, context)
244 | } catch (_: Exception) { }
245 |
246 | if (cursor == null || !cursor.moveToFirst()) return@launch
247 |
248 | // Get all of colum index
249 | with(cursor) {
250 | val dataIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATA) ?: -1
251 | val dateAddedIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED) ?: -1
252 | val widthIndex = getColumnIndex(MediaStore.Images.ImageColumns.WIDTH) ?: -1
253 | val heightIndex = getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT) ?: -1
254 |
255 | // Handle media row data
256 | // File path
257 | val filePath = cursor.getScreenShotFilePath(dataIndex)
258 | if (!isFilePathLegal(filePath)) return@with
259 | // File Date Added
260 | val dateAdded = cursor.getScreenShotFileDateAdded(dateAddedIndex)
261 | if (!isFileCreationTimeLegal(dateAdded, startListenTime)) return@with
262 | // File Size
263 | val (width, height) = cursor.getScreenShotFileSize(filePath, widthIndex, heightIndex)
264 | if (!isFileSizeLegal(width, height)) return@with
265 |
266 | handleScreenShot(filePath)
267 | }
268 | if (!cursor.isClosed) cursor.close()
269 | }
270 | }
271 | ```
272 | ## Android 14及以上机型
273 | 使用系统API`Activity.ScreenCaptureCallback`进行监控,但是由于没有全部相册权限获取不到截屏文件的具体路径,所以只能实现一些不依赖路径的动作(如埋点上报等)
274 | ### 1. 声明相关callback
275 | ```kotlin
276 | private var screenShotCaptureCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
277 | Activity.ScreenCaptureCallback {
278 | // handle screenshot
279 | }
280 | } else null
281 | ```
282 | ### 2. 注册监听
283 | 在activity启动的时候开始对截屏进行监听,在activity消失的时候结束对截屏的监听,时机与使用媒体库监听时机一样
284 | ```kotlin
285 | private fun registerCallback(activity: Activity) {
286 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
287 | screenShotCaptureCallback?.let { callback ->
288 | activity.registerScreenCaptureCallback(activity.mainExecutor, callback)
289 | }
290 | }
291 | }
292 |
293 | private fun unregisterCallback(activity: Activity) {
294 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
295 | screenShotCaptureCallback?.let { callback ->
296 | try {
297 | activity.unregisterScreenCaptureCallback(callback)
298 | startListenTime = 0
299 | } catch (_: IllegalStateException) {}
300 | }
301 | }
302 | }
303 | ```
304 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | }
5 |
6 | android {
7 | namespace = "com.android.screenshot"
8 | compileSdk = 34
9 |
10 | defaultConfig {
11 | applicationId = "com.android.screenshot"
12 | minSdk = 24
13 | targetSdk = 34
14 | versionCode = 1
15 | versionName = "1.0"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | isMinifyEnabled = false
23 | proguardFiles(
24 | getDefaultProguardFile("proguard-android-optimize.txt"),
25 | "proguard-rules.pro"
26 | )
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility = JavaVersion.VERSION_1_8
31 | targetCompatibility = JavaVersion.VERSION_1_8
32 | }
33 | kotlinOptions {
34 | jvmTarget = "1.8"
35 | }
36 | buildFeatures {
37 | viewBinding = true
38 | }
39 | }
40 |
41 | dependencies {
42 |
43 | implementation(libs.androidx.core.ktx)
44 | implementation(libs.androidx.appcompat)
45 | implementation(libs.material)
46 | implementation(libs.androidx.constraintlayout)
47 | implementation(libs.androidx.navigation.fragment.ktx)
48 | implementation(libs.androidx.navigation.ui.ktx)
49 | testImplementation(libs.junit)
50 | androidTestImplementation(libs.androidx.junit)
51 | androidTestImplementation(libs.androidx.espresso.core)
52 | }
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/android/screenshot/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.android.screenshot", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/activity/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.activity
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import com.android.screenshot.utils.AppMonitor
5 | import com.android.screenshot.utils.Logger
6 |
7 | /**
8 | * Created by Debin Kong on 2024/3/22
9 | * @author Debin Kong
10 | */
11 | abstract class BaseActivity : AppCompatActivity(), Logger {
12 | override fun onResume() {
13 | super.onResume()
14 | AppMonitor.setCurrentActivity(this)
15 | }
16 |
17 | override fun onPause() {
18 | super.onPause()
19 | AppMonitor.setCurrentActivity(null)
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/activity/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.activity
2 |
3 | import android.os.Bundle
4 | import androidx.recyclerview.widget.LinearLayoutManager
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.android.screenshot.databinding.ActivityMainBinding
7 | import com.android.screenshot.functions.ScreenShotStartFunction
8 | import com.android.screenshot.adapter.FunctionAdapter
9 | import com.android.screenshot.functions.ScreenShotEndFunction
10 |
11 | class MainActivity : BaseActivity() {
12 |
13 | private lateinit var binding: ActivityMainBinding
14 |
15 | override fun onCreate(savedInstanceState: Bundle?) {
16 | super.onCreate(savedInstanceState)
17 | binding = ActivityMainBinding.inflate(layoutInflater)
18 | setContentView(binding.root)
19 | initView()
20 | }
21 |
22 | private fun initView() {
23 | val functionsRecyclerView = binding.functionsContainer
24 | functionsRecyclerView.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false) // 设置布局管理器
25 | functionsRecyclerView.adapter = FunctionAdapter(initFunctions(), binding.functionsView)
26 | }
27 |
28 | private fun initFunctions() = listOf(
29 | ScreenShotStartFunction(this),
30 | ScreenShotEndFunction(this),
31 | )
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/activity/PermissionsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.activity
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import androidx.core.app.ActivityCompat
7 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
8 | import com.android.screenshot.R
9 |
10 | /**
11 | * Created by Debin Kong on 2024/3/25
12 | * @author Debin Kong
13 | */
14 | class PermissionsActivity : BaseActivity() {
15 |
16 | companion object {
17 | const val PERMISSIONS_REQUEST_CODE = 1
18 | const val EXTRA_PERMISSIONS = "extra_permissions"
19 | const val ACTION_PERMISSIONS_RESPONSE = "action_permissions_response"
20 | const val EXTRA_PERMISSIONS_RESULT = "extra_permissions_result"
21 |
22 | fun startForResult(context: Context, permissions: Array) {
23 | val intent = Intent(context, PermissionsActivity::class.java).apply {
24 | putExtra(EXTRA_PERMISSIONS, permissions)
25 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
26 | }
27 | context.startActivity(intent)
28 | }
29 | }
30 |
31 | override fun onCreate(savedInstanceState: Bundle?) {
32 | super.onCreate(savedInstanceState)
33 | setTheme(R.style.Theme_Transparent)
34 | val permissions = intent.getStringArrayExtra(EXTRA_PERMISSIONS)
35 | if (permissions.isNullOrEmpty()) {
36 | finish()
37 | } else {
38 | ActivityCompat.requestPermissions(this, permissions, PERMISSIONS_REQUEST_CODE)
39 | }
40 | overridePendingTransition(0, 0)
41 | }
42 |
43 | override fun finish() {
44 | super.finish()
45 | overridePendingTransition(0, 0)
46 | }
47 |
48 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
49 | super.onRequestPermissionsResult(requestCode, permissions, grantResults)
50 |
51 | if (requestCode == PERMISSIONS_REQUEST_CODE) {
52 | val intent = Intent(ACTION_PERMISSIONS_RESPONSE).apply {
53 | putExtra(EXTRA_PERMISSIONS, permissions)
54 | putExtra(EXTRA_PERMISSIONS_RESULT, grantResults)
55 | }
56 | LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
57 | finish()
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/adapter/FunctionAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.adapter
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.RecyclerView
5 | import com.android.screenshot.R
6 | import com.android.screenshot.functions.IFunction
7 | import com.android.screenshot.utils.Logger
8 |
9 | /**
10 | * Created by Debin Kong on 2024/3/22
11 | * @author Debin Kong
12 | */
13 | class FunctionAdapter(
14 | private val functions: List,
15 | private val viewGroup: ViewGroup
16 | ) : RecyclerView.Adapter(), Logger {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FunctionViewHolder {
19 | return FunctionViewHolder(FunctionButton(parent.context))
20 | }
21 |
22 | override fun getItemCount(): Int = functions.size
23 |
24 | override fun onBindViewHolder(holder: FunctionViewHolder, position: Int) {
25 | functions.getOrNull(position)?.let { function ->
26 | info { "set $position, label: ${function.label()}" }
27 | holder.setUpView(function, viewGroup)
28 | }
29 | }
30 |
31 | override fun getItemViewType(position: Int): Int = R.layout.view_function_item
32 |
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/adapter/FunctionButton.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.adapter
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.view.LayoutInflater
6 | import android.widget.FrameLayout
7 | import com.android.screenshot.databinding.ViewFunctionItemBinding
8 |
9 | /**
10 | * Created by Debin Kong on 2024/3/22
11 | * @author Debin Kong
12 | */
13 | class FunctionButton(
14 | context: Context,
15 | attrs: AttributeSet? = null,
16 | defStyleAttr: Int = 0
17 | ) : FrameLayout(context, attrs, defStyleAttr) {
18 |
19 | private val binding: ViewFunctionItemBinding
20 |
21 | init {
22 | binding = ViewFunctionItemBinding.inflate(LayoutInflater.from(context), this, true)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/adapter/FunctionViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.adapter
2 |
3 | import android.view.ViewGroup
4 | import android.widget.FrameLayout
5 | import android.widget.TextView
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.android.screenshot.R
8 | import com.android.screenshot.functions.IFunction
9 | import com.android.screenshot.utils.Logger
10 |
11 | /**
12 | * Created by Debin Kong on 2024/3/22
13 | * @author Debin Kong
14 | */
15 | class FunctionViewHolder(itemView: FunctionButton) : RecyclerView.ViewHolder(itemView), Logger {
16 |
17 | fun setUpView(function: IFunction, viewGroup: ViewGroup) {
18 | val labelView = itemView.findViewById(R.id.function_label)
19 | val buttonContainer = itemView.findViewById(R.id.function_container)
20 |
21 | labelView?.setText(function.label())
22 | buttonContainer?.setOnClickListener {
23 | function.clickAction()
24 | viewGroup.removeAllViews()
25 | function.view()?.let { view ->
26 | viewGroup.addView(view)
27 | }
28 | }
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/functions/AbsFunction.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.functions
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import com.android.screenshot.utils.Logger
6 |
7 | /**
8 | * Created by Debin Kong on 2024/3/22
9 | * @author Debin Kong
10 | */
11 | abstract class AbsFunction(
12 | protected val context: Context
13 | ): IFunction, Logger {
14 | override fun view(): View? = null
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/functions/IFunction.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.functions
2 |
3 | import android.view.View
4 | import androidx.annotation.StringRes
5 |
6 | /**
7 | * Created by Debin Kong on 2024/3/25
8 | * @author Debin Kong
9 | */
10 | interface IFunction {
11 |
12 | @StringRes
13 | fun label(): Int
14 |
15 | fun clickAction()
16 |
17 | fun view(): View?
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/functions/ScreenShotEndFunction.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.functions
2 |
3 | import android.content.Context
4 | import com.android.screenshot.R
5 | import com.android.screenshot.screenshot.ScreenShotEventDispatcher
6 | import com.android.screenshot.screenshot.ScreenShotMonitor
7 | import com.android.screenshot.utils.AppMonitor
8 | import com.android.screenshot.utils.activity
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.launch
12 | import java.lang.ref.WeakReference
13 |
14 | /**
15 | * Created by Debin Kong on 2024/3/25
16 | * @author Debin Kong
17 | */
18 | class ScreenShotEndFunction(context: Context): AbsFunction(context) {
19 | override fun label(): Int = R.string.function_end_monitor_screenshot
20 |
21 | override fun clickAction() {
22 | CoroutineScope(Dispatchers.IO).launch {
23 | ScreenShotMonitor.instance().stopListen(WeakReference(context.activity ?: AppMonitor.getCurrentActivity()))
24 | ScreenShotEventDispatcher.refreshScreenShotConfig()
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/functions/ScreenShotStartFunction.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.functions
2 |
3 | import android.content.Context
4 | import android.view.View
5 | import com.android.screenshot.R
6 | import com.android.screenshot.screenshot.ScreenShotEventDispatcher
7 | import com.android.screenshot.screenshot.ScreenShotListener
8 | import com.android.screenshot.screenshot.ScreenShotMonitor
9 | import com.android.screenshot.screenshot.ScreenShotPreviewView
10 | import com.android.screenshot.utils.AppMonitor
11 | import com.android.screenshot.utils.Weak
12 | import com.android.screenshot.utils.activity
13 | import kotlinx.coroutines.CoroutineScope
14 | import kotlinx.coroutines.Dispatchers
15 | import kotlinx.coroutines.launch
16 | import java.lang.ref.WeakReference
17 |
18 | /**
19 | * Created by Debin Kong on 2024/3/25
20 | * @author Debin Kong
21 | */
22 | class ScreenShotStartFunction(context: Context): AbsFunction(context) {
23 | val previewView by Weak { ScreenShotPreviewView(context) }
24 |
25 | override fun label(): Int = R.string.function_start_monitor_screenshot
26 |
27 | override fun clickAction() {
28 | CoroutineScope(Dispatchers.IO).launch {
29 | ScreenShotMonitor.instance().startListen(WeakReference(context.activity ?: AppMonitor.getCurrentActivity()))
30 | ScreenShotEventDispatcher.setScreenShotConfig(object : ScreenShotListener() {
31 | override fun onShot(imagePath: String?) {
32 | previewView?.updateSetting(imagePath)
33 | info { "screenshot path: $imagePath" }
34 | }
35 | })
36 | }
37 | }
38 |
39 | override fun view(): View? = previewView
40 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/screenshot/ScreenShotEventDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.screenshot
2 |
3 | import android.content.ContentResolver
4 | import android.content.Context
5 | import android.database.Cursor
6 | import android.graphics.BitmapFactory
7 | import android.graphics.Point
8 | import android.net.Uri
9 | import android.os.Build
10 | import android.os.Bundle
11 | import android.provider.MediaStore
12 | import android.text.TextUtils
13 | import android.view.Display
14 | import android.view.WindowManager
15 | import com.android.screenshot.utils.Logger
16 | import kotlinx.coroutines.CoroutineScope
17 | import kotlinx.coroutines.Dispatchers
18 | import kotlinx.coroutines.launch
19 | import java.util.Locale
20 |
21 | /**
22 | * Created by Debin Kong on 2024/3/25
23 | * @author Debin Kong
24 | */
25 | object ScreenShotEventDispatcher: Logger {
26 |
27 | private var screenRealSize: Point? = null
28 |
29 | /**
30 | * Listener brought with Config configuration, only holds one and will be replaced by a new page config at any time
31 | */
32 | private var screenShotConfigListener: ScreenShotListener? = null
33 |
34 | /**
35 | * ### Set screenshot config
36 | * @param enterFrom String
37 | * @param screenShotListener [ScreenShotListener]?
38 | */
39 | fun setScreenShotConfig(screenShotListener: ScreenShotListener?) {
40 | screenShotConfigListener = screenShotListener
41 | }
42 |
43 | /**
44 | * ### Refresh screenshot config
45 | */
46 | fun refreshScreenShotConfig() {
47 | screenShotConfigListener = null
48 | }
49 |
50 | /**
51 | * ### Handle media content change (get 1st data and determine whether it is a screenshot file)
52 | * @param contentUri Uri
53 | * @param context Context?
54 | * @param startListenTime Long
55 | */
56 | fun handleMediaContentChange(
57 | contentUri: Uri,
58 | context: Context?,
59 | startListenTime: Long?
60 | ) {
61 | CoroutineScope(Dispatchers.IO).launch {
62 | if (context == null) {
63 | error { "context is null" }
64 | return@launch
65 | }
66 | if (screenRealSize == null) screenRealSize = getRealScreenSize(context)
67 |
68 | var cursor: Cursor? = null
69 |
70 | try {
71 | cursor = getContentResolverCursor(contentUri, context)
72 | } catch (_: Exception) { }
73 |
74 | if (cursor == null || !cursor.moveToFirst()) {
75 | error { "cannot move to first" }
76 | return@launch
77 | }
78 |
79 | // Get all of colum index
80 | with(cursor) {
81 | val dataIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATA) ?: -1
82 | val dateAddedIndex = getColumnIndex(MediaStore.Images.ImageColumns.DATE_ADDED) ?: -1
83 | val widthIndex = getColumnIndex(MediaStore.Images.ImageColumns.WIDTH) ?: -1
84 | val heightIndex = getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT) ?: -1
85 |
86 | // Handle media row data
87 | // File path
88 | val filePath = cursor.getScreenShotFilePath(dataIndex)
89 | if (!isFilePathLegal(filePath)) return@with
90 | // File Date Added
91 | val dateAdded = cursor.getScreenShotFileDateAdded(dateAddedIndex)
92 | if (!isFileCreationTimeLegal(dateAdded, startListenTime)) return@with
93 | // File Size
94 | val (width, height) = cursor.getScreenShotFileSize(filePath, widthIndex, heightIndex)
95 | if (!isFileSizeLegal(width, height)) return@with
96 | handleScreenShot(filePath)
97 | }
98 | if (!cursor.isClosed) cursor.close()
99 | }
100 | }
101 |
102 | /**
103 | * ### Handle screenshot
104 | * @param filePath String
105 | */
106 | fun handleScreenShot(filePath: String?) {
107 | CoroutineScope(Dispatchers.Main).launch {
108 | screenShotConfigListener?.onShot(filePath)
109 | }
110 | }
111 |
112 | /**
113 | * ### Get screenshot file path
114 | * @receiver [Cursor]
115 | * @return [String]
116 | */
117 | private fun Cursor.getScreenShotFilePath(dataIndex: Int): String = this.getString(dataIndex) ?: ""
118 |
119 | /**
120 | * ### Get screenshot file date added
121 | * @receiver [Cursor]
122 | * @return [Long]
123 | */
124 | private fun Cursor.getScreenShotFileDateAdded(dateAddedIndex: Int): Long = (this.getLong(dateAddedIndex) * 1000) ?: 0L
125 |
126 | /**
127 | * ### Get screenshot file size
128 | * @receiver [Cursor]
129 | * @return [Pair]
130 | */
131 | private fun Cursor.getScreenShotFileSize(filePath: String, widthIndex: Int, heightIndex: Int): Pair =
132 | if (widthIndex >= 0 && heightIndex >= 0) {
133 | Pair(this.getInt(widthIndex) ?: 0, this.getInt(heightIndex) ?: 0)
134 | } else {
135 | // Before API 16, the width and height need to be obtained manually.
136 | val size = getImageSize(filePath)
137 | Pair(size.x ?: 0, size.y ?: 0)
138 | }
139 |
140 | /**
141 | * ### Determine the file path
142 | * @param filePath String
143 | * @return Boolean
144 | */
145 | private fun isFilePathLegal(filePath: String?): Boolean {
146 | // File path is not empty
147 | if (filePath == null || TextUtils.isEmpty(filePath)) {
148 | warn { "error: path $filePath" }
149 | return false
150 | }
151 |
152 | // File path contains screenshot KEYWORDS
153 | var hasValidScreenShot = false
154 | val lowerPath = filePath.lowercase(Locale.getDefault())
155 | for (keyWork: String in KEYWORDS) {
156 | if (lowerPath.contains(keyWork)) {
157 | hasValidScreenShot = true
158 | break
159 | }
160 | }
161 | return hasValidScreenShot
162 | }
163 |
164 | /**
165 | * ### Determine the file creation time
166 | * If the time added to the database is before the start of listening or the difference is greater than 10 seconds from the current time, the screenshot file is considered not the current file.
167 | * @param dateAdded Long
168 | * @param startListenTime Long
169 | * @return Boolean
170 | */
171 | private fun isFileCreationTimeLegal(dateAdded: Long?, startListenTime: Long?) =
172 | if (dateAdded == null
173 | || startListenTime == null
174 | || dateAdded / 1000 < startListenTime / 1000
175 | || (System.currentTimeMillis() - dateAdded) > MAX_COST_TIME
176 | ) {
177 | warn {
178 | "error: time doesn't match\n" +
179 | " dateAdded: $dateAdded \n" +
180 | " startListenTime: $startListenTime \n"
181 | }
182 | false
183 | } else true
184 |
185 | /**
186 | * ### Determine the file size.
187 | * If the image size exceeds the screen, it is considered that the current screenshot file is not a local screenshot.
188 | * @param width Int
189 | * @param height Int
190 | * @return Boolean
191 | */
192 | private fun isFileSizeLegal(width: Int?, height: Int?) =
193 | screenRealSize?.let {
194 | if (width == null || height == null) {
195 | false
196 | } else if (!((width <= it.x && height <= it.y) || (height <= it.x && width <= it.y))) {
197 | warn { "error: size" }
198 | false
199 | } else {
200 | true
201 | }
202 | } ?: false
203 |
204 |
205 | /**
206 | * ### Get real screen size
207 | * @param context Context
208 | * @return [Point]?
209 | */
210 | private fun getRealScreenSize(context: Context): Point? {
211 | var screenSize: Point? = null
212 | try {
213 | screenSize = Point()
214 | val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
215 | val defaultDisplay = windowManager.defaultDisplay
216 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
217 | defaultDisplay.getRealSize(screenSize)
218 | } else {
219 | try {
220 | val rawWidth = Display::class.java.getMethod("getRawWidth")
221 | val rawHeight = Display::class.java.getMethod("getRawHeight")
222 | screenSize.set(
223 | (rawWidth.invoke(defaultDisplay) as Int),
224 | (rawHeight.invoke(defaultDisplay) as Int),
225 | )
226 | } catch (_: Exception) {
227 | screenSize.set(defaultDisplay.width, defaultDisplay.height)
228 | }
229 | }
230 | } catch (_: Exception) { }
231 | return screenSize
232 | }
233 |
234 | /**
235 | * ### Get image size
236 | * @param imagePath String
237 | * @return [Point]
238 | */
239 | private fun getImageSize(imagePath: String): Point {
240 | val options = BitmapFactory.Options()
241 | options.inJustDecodeBounds = true
242 | BitmapFactory.decodeFile(imagePath, options)
243 | return Point(options.outWidth, options.outHeight)
244 | }
245 |
246 | /**
247 | * ### Get content cursor
248 | * @param contentUri Uri
249 | * @param context Context
250 | * @param maxCount Int
251 | * @return [Cursor]
252 | */
253 | private fun getContentResolverCursor(
254 | contentUri: Uri,
255 | context: Context,
256 | maxCount: Int = 1
257 | ) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
258 | val bundle = Bundle().apply {
259 | putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(MediaStore.Images.ImageColumns.DATE_MODIFIED))
260 | putInt(ContentResolver.QUERY_ARG_SORT_DIRECTION, ContentResolver.QUERY_SORT_DIRECTION_DESCENDING)
261 | putInt(ContentResolver.QUERY_ARG_LIMIT, maxCount)
262 | }
263 | context.contentResolver.query(
264 | contentUri,
265 | MEDIA_PROJECTIONS_API_16,
266 | bundle,
267 | null,
268 | )
269 | } else {
270 | context.contentResolver.query(
271 | contentUri,
272 | MEDIA_PROJECTIONS,
273 | null,
274 | null,
275 | "${MediaStore.Images.ImageColumns.DATE_MODIFIED} desc limit ${maxCount}",
276 | )
277 | }
278 |
279 | private class ScreenShotTrackerLRUMap :
280 | LinkedHashMap(MAX_CAPACITY, LOAD_FACTOR, true) {
281 | override fun removeEldestEntry(eldest: MutableMap.MutableEntry?): Boolean {
282 | return size > MAX_CAPACITY
283 | }
284 |
285 | companion object {
286 | private const val MAX_CAPACITY = 30
287 | private const val LOAD_FACTOR = 0.75f
288 | }
289 | }
290 |
291 | private const val MAX_COST_TIME = 10 * 1000
292 | private val MEDIA_PROJECTIONS = arrayOf(
293 | MediaStore.Images.ImageColumns.DATA,
294 | MediaStore.Images.ImageColumns.DATE_ADDED,
295 | )
296 | private val MEDIA_PROJECTIONS_API_16 = arrayOf(
297 | MediaStore.Images.ImageColumns.DATA,
298 | MediaStore.Images.ImageColumns.DATE_ADDED,
299 | MediaStore.Images.ImageColumns.WIDTH,
300 | MediaStore.Images.ImageColumns.HEIGHT,
301 | )
302 | private val KEYWORDS = arrayOf(
303 | "screenshot", "screen_shot", "screen-shot", "screen shot",
304 | "screencapture", "screen_capture", "screen-capture", "screen capture",
305 | "screencap", "screen_cap", "screen-cap", "screen cap",
306 | )
307 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/screenshot/ScreenShotListener.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.screenshot
2 |
3 | /**
4 | * Created by Debin Kong on 2024/3/25
5 | * @author Debin Kong
6 | */
7 | abstract class ScreenShotListener {
8 | /**
9 | * ### Screen shot
10 | * @param imagePath screenshot image file path
11 | */
12 | abstract fun onShot(imagePath: String?)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/screenshot/ScreenShotMonitor.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.screenshot
2 |
3 | import android.Manifest
4 | import android.app.Activity
5 | import android.content.pm.PackageManager
6 | import android.database.ContentObserver
7 | import android.net.Uri
8 | import android.os.Build
9 | import android.os.Handler
10 | import android.os.Looper
11 | import android.provider.MediaStore
12 | import androidx.core.content.ContextCompat
13 | import com.android.screenshot.screenshot.ScreenShotEventDispatcher.handleMediaContentChange
14 | import com.android.screenshot.screenshot.ScreenShotEventDispatcher.handleScreenShot
15 | import com.android.screenshot.utils.Logger
16 | import com.android.screenshot.utils.PermissionListener
17 | import com.android.screenshot.utils.PermissionsUtils
18 | import com.android.screenshot.utils.Weak
19 | import kotlinx.coroutines.Dispatchers
20 | import kotlinx.coroutines.withContext
21 | import java.lang.ref.WeakReference
22 |
23 | /**
24 | * Created by Debin Kong on 2024/3/25
25 | * @author Debin Kong
26 | */
27 | class ScreenShotMonitor: Logger {
28 |
29 | private var isHasScreenShotListen = false
30 |
31 | private var isHasScreenShotCaptureCallback = false
32 |
33 | private var internalObserver: MediaContentObserver? = null
34 |
35 | private var externalObserver: MediaContentObserver? = null
36 |
37 | private val uiHandler = Handler(Looper.getMainLooper())
38 |
39 | private var startListenTime: Long = 0
40 | set(value) {
41 | field = value
42 | info { "startListenTime change to $value" }
43 | }
44 |
45 | private var screenShotCaptureCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
46 | Activity.ScreenCaptureCallback {
47 | handleScreenShot(null)
48 | }
49 | } else null
50 |
51 | private var activity: Activity? by Weak { null }
52 |
53 | companion object {
54 | private val monitor: ScreenShotMonitor by lazy { ScreenShotMonitor() }
55 |
56 | /**
57 | * ### INSTANCE
58 | * @return [ScreenShotMonitor]
59 | */
60 | fun instance() = monitor
61 | }
62 |
63 | suspend fun startListen(weakActivity: WeakReference) = withContext(Dispatchers.IO) {
64 | weakActivity.get()?.let { activity ->
65 | this@ScreenShotMonitor.activity = activity
66 | info { "Activity[${activity}]: Try to start screen shot listener: isHasScreenShotListen: $isHasScreenShotListen isHasScreenShotCaptureCallback: $isHasScreenShotCaptureCallback" }
67 | if (!isHasScreenShotListen && !isHasScreenShotCaptureCallback) {
68 | val permission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
69 | Manifest.permission.READ_MEDIA_IMAGES
70 | else
71 | Manifest.permission.READ_EXTERNAL_STORAGE
72 |
73 | try {
74 | if (ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED) {
75 | registerObserver(activity)
76 | } else {
77 | PermissionsUtils.request(activity, arrayOf(permission), object :
78 | PermissionListener {
79 | override fun onPermissionGrant(permissions: Array) { registerObserver(activity) }
80 | override fun onPermissionDenied(permissions: Array) { registerCallback(activity) }
81 | })
82 | }
83 | } catch (exception: Exception) { error { exception.toString() } }
84 | }
85 | }
86 | }
87 |
88 | suspend fun stopListen(weakActivity: WeakReference) = withContext(Dispatchers.IO) {
89 | weakActivity.get()?.let { activity ->
90 | this@ScreenShotMonitor.activity = null
91 | info { "Activity[${activity}]: Try to stop screen shot listener: isHasScreenShotListen: $isHasScreenShotListen isHasScreenShotCaptureCallback: $isHasScreenShotCaptureCallback" }
92 | if (isHasScreenShotListen) { unregisterObserver(activity) }
93 | if (isHasScreenShotCaptureCallback) { unregisterCallback(activity) }
94 | }
95 | }
96 |
97 | private fun registerObserver(activity: Activity) {
98 | internalObserver = MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, uiHandler)
99 | externalObserver = MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, uiHandler)
100 | startListenTime = System.currentTimeMillis()
101 | internalObserver?.let {
102 | activity.applicationContext.contentResolver.registerContentObserver(
103 | MediaStore.Images.Media.INTERNAL_CONTENT_URI,
104 | Build.VERSION.SDK_INT > Build.VERSION_CODES.P, it,
105 | )
106 | }
107 | externalObserver?.let {
108 | activity.applicationContext.contentResolver.registerContentObserver(
109 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
110 | Build.VERSION.SDK_INT > Build.VERSION_CODES.P, it,
111 | )
112 | }
113 | info {"[${activity}] start listen" }
114 | isHasScreenShotListen = true
115 | }
116 |
117 | private fun unregisterObserver(activity: Activity) {
118 | try {
119 | internalObserver?.let {
120 | activity.contentResolver.unregisterContentObserver(it)
121 | }
122 | } catch (e: Exception) { error { e.toString() } }
123 | internalObserver = null
124 |
125 | try {
126 | externalObserver?.let {
127 | activity.contentResolver.unregisterContentObserver(it)
128 | }
129 | } catch (e: Exception) { error { e.toString() } }
130 | externalObserver = null
131 |
132 | info { "[${activity}] Stop listen" }
133 | isHasScreenShotListen = false
134 | startListenTime = 0
135 | }
136 |
137 | private fun registerCallback(activity: Activity) {
138 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
139 | screenShotCaptureCallback?.let { callback ->
140 | activity.registerScreenCaptureCallback(activity.mainExecutor, callback)
141 | isHasScreenShotCaptureCallback = true
142 | info {"[${activity}] start listen" }
143 | }
144 | }
145 | }
146 |
147 | private fun unregisterCallback(activity: Activity) {
148 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
149 | screenShotCaptureCallback?.let { callback ->
150 | try {
151 | activity.unregisterScreenCaptureCallback(callback)
152 | isHasScreenShotCaptureCallback = false
153 | startListenTime = 0
154 | info { "[${activity}] Stop listen" }
155 | } catch (e: IllegalStateException) {
156 | isHasScreenShotCaptureCallback = false
157 | error { e.toString() }
158 | }
159 | }
160 | }
161 | }
162 |
163 | private inner class MediaContentObserver(private val contentUri: Uri, handler: Handler?) : ContentObserver(handler) {
164 | override fun onChange(selfChange: Boolean) {
165 | super.onChange(selfChange)
166 | handleMediaContentChange(contentUri, activity, startListenTime)
167 | }
168 | }
169 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/screenshot/ScreenShotPreviewView.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.screenshot
2 |
3 | import android.content.Context
4 | import android.graphics.Bitmap
5 | import android.graphics.BitmapFactory
6 | import android.graphics.Canvas
7 | import android.graphics.Paint
8 | import android.graphics.Rect
9 | import android.util.AttributeSet
10 | import android.view.LayoutInflater
11 | import android.widget.FrameLayout
12 | import androidx.annotation.ColorRes
13 | import androidx.annotation.Px
14 | import com.android.screenshot.R
15 | import com.android.screenshot.databinding.ViewScreenshotPreviewBinding
16 | import com.android.screenshot.utils.dp
17 | import kotlinx.coroutines.CoroutineScope
18 | import kotlinx.coroutines.Dispatchers
19 | import kotlinx.coroutines.launch
20 | import kotlinx.coroutines.withContext
21 |
22 | /**
23 | * Created by Debin Kong on 2024/3/26
24 | * @author Debin Kong
25 | */
26 | class ScreenShotPreviewView(
27 | context: Context,
28 | attrs: AttributeSet? = null,
29 | defStyleAttr: Int = 0
30 | ) : FrameLayout(context, attrs, defStyleAttr) {
31 |
32 | private val binding: ViewScreenshotPreviewBinding
33 |
34 | init {
35 | binding = ViewScreenshotPreviewBinding.inflate(LayoutInflater.from(context), this, true)
36 | }
37 |
38 | fun updateSetting(path: String?) {
39 | binding.screenshotPath.text = path ?: context.getString(R.string.empty_path)
40 | CoroutineScope(Dispatchers.Main).launch {
41 | path?.let {
42 | val bitmap = getScreenShotBitmap(it)
43 | binding.screenshotPreview.setImageBitmap(bitmap)
44 | }
45 | }
46 | }
47 |
48 | private suspend fun getScreenShotBitmap(
49 | path: String,
50 | @Px borderWidth: Int = 2.dp,
51 | @ColorRes borderColor: Int = R.color.black
52 | ): Bitmap = withContext(Dispatchers.IO) {
53 | val originalBitmap = BitmapFactory.decodeFile(path)
54 | val newBitmapWidth = originalBitmap.width + borderWidth * 2
55 | val newBitmapHeight = originalBitmap.height + borderWidth * 2
56 | val newBitmap = Bitmap.createBitmap(newBitmapWidth, newBitmapHeight, originalBitmap.config)
57 | val canvas = Canvas(newBitmap)
58 | val paint = Paint().apply {
59 | color = borderColor
60 | style = Paint.Style.STROKE
61 | strokeWidth = borderWidth.toFloat()
62 | }
63 | val borderRect = Rect(
64 | borderWidth / 2,
65 | borderWidth / 2,
66 | newBitmapWidth - borderWidth / 2,
67 | newBitmapHeight - borderWidth / 2
68 | )
69 | canvas.drawRect(borderRect, paint)
70 | val originalBitmapRect = Rect(0, 0, originalBitmap.width, originalBitmap.height)
71 | val newBitmapRect = Rect(borderWidth, borderWidth, newBitmapWidth - borderWidth, newBitmapHeight - borderWidth)
72 | canvas.drawBitmap(originalBitmap, originalBitmapRect, newBitmapRect, null)
73 | return@withContext newBitmap
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/utils/AppMonitor.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.utils
2 |
3 | import android.app.Activity
4 | import java.lang.ref.WeakReference
5 |
6 | /**
7 | * Created by Debin Kong on 2024/3/25
8 | * @author Debin Kong
9 | */
10 | object AppMonitor {
11 |
12 | private var currentActivity: WeakReference? = null
13 |
14 | fun setCurrentActivity(activity: Activity?) {
15 | currentActivity = if (activity == null) {
16 | null
17 | } else {
18 | WeakReference(activity)
19 | }
20 | }
21 |
22 | fun getCurrentActivity(): Activity? {
23 | return currentActivity?.get()
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/utils/Extension.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.utils
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.ContextWrapper
6 | import android.content.res.Resources
7 | import android.util.TypedValue
8 | import kotlin.math.roundToInt
9 |
10 | /**
11 | * Created by Debin Kong on 2024/3/25
12 | * @author Debin Kong
13 | */
14 | val Context.activity: Activity?
15 | get() {
16 | var c: Context? = this
17 | while (c != null) {
18 | c = when (c) {
19 | is Activity -> return c
20 | is ContextWrapper -> c.baseContext
21 | else -> return null
22 | }
23 | }
24 | return null
25 | }
26 |
27 | val Number.dpFloat
28 | get() = TypedValue.applyDimension(
29 | TypedValue.COMPLEX_UNIT_DIP,
30 | toFloat(),
31 | Resources.getSystem().displayMetrics
32 | )
33 |
34 |
35 | inline val Number.dp
36 | get() = dpFloat.roundToInt()
37 |
38 |
39 | val Number.spFloat
40 | get() = TypedValue.applyDimension(
41 | TypedValue.COMPLEX_UNIT_SP,
42 | this.toFloat(),
43 | Resources.getSystem().displayMetrics
44 | )
45 |
46 | inline val Number.sp
47 | get() = spFloat.roundToInt()
48 |
49 |
50 | inline val Number.px
51 | get() = this.toInt()
52 |
53 |
54 | fun Context.getScreenWidth(): Int {
55 | val dm = resources.displayMetrics
56 | return dm?.widthPixels ?: 0
57 | }
58 |
59 | fun Context.getScreenHeight(): Int {
60 | val dm = resources.displayMetrics
61 | return dm?.heightPixels ?: 0
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/utils/Logger.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.utils
2 |
3 | import android.util.Log
4 |
5 | /**
6 | * Created by Debin Kong on 2024/3/25
7 | * @author Debin Kong
8 | */
9 | interface Logger {
10 | fun tag(): String = this.javaClass.simpleName
11 |
12 | fun debug(func: () -> String) = Log.d("Screenshot-" + tag(), func.invoke())
13 |
14 | fun info(func: () -> String) = Log.i("Screenshot-" + tag(), func.invoke())
15 |
16 | fun warn(func: () -> String) = Log.w("Screenshot-" + tag(), func.invoke())
17 |
18 | fun error(func: () -> String) = Log.e("Screenshot-" + tag(), func.invoke())
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/utils/PermissionsUtils.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.utils
2 |
3 | import android.content.BroadcastReceiver
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.content.pm.PackageManager
8 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
9 | import com.android.screenshot.activity.PermissionsActivity
10 |
11 | /**
12 | * Created by Debin Kong on 2024/3/25
13 | * @author Debin Kong
14 | */
15 | object PermissionsUtils: Logger {
16 |
17 | fun request(context: Context, permission: Array, listener: PermissionListener) {
18 | // 注册接收权限结果的 BroadcastReceiver
19 | val permissionReceiver = object : BroadcastReceiver() {
20 | override fun onReceive(context: Context, intent: Intent) {
21 | try {
22 | val permissions = intent.getStringArrayExtra(PermissionsActivity.EXTRA_PERMISSIONS) ?: arrayOf()
23 | val grantResults = intent.getIntArrayExtra(PermissionsActivity.EXTRA_PERMISSIONS_RESULT)
24 | grantResults?.forEach { result ->
25 | if (result != PackageManager.PERMISSION_GRANTED) {
26 | listener.onPermissionDenied(permissions)
27 | return
28 | }
29 | }
30 | listener.onPermissionGrant(permissions)
31 | } catch (e: Exception) { error { e.toString() } } finally {
32 | LocalBroadcastManager.getInstance(context).unregisterReceiver(this)
33 | }
34 | }
35 | }
36 |
37 | LocalBroadcastManager.getInstance(context).registerReceiver(permissionReceiver, IntentFilter(
38 | PermissionsActivity.ACTION_PERMISSIONS_RESPONSE))
39 | PermissionsActivity.startForResult(context, permission)
40 | }
41 | }
42 |
43 | interface PermissionListener {
44 | fun onPermissionGrant(permissions: Array)
45 | fun onPermissionDenied(permissions: Array)
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/screenshot/utils/Weak.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot.utils
2 |
3 | import java.lang.ref.WeakReference
4 | import kotlin.reflect.KProperty
5 |
6 | /**
7 | * Created by Debin Kong on 2024/3/25
8 | * @author Debin Kong
9 | */
10 | class Weak(initializer: () -> T?) {
11 | private var weakReference = WeakReference(initializer())
12 |
13 | constructor() : this({
14 | null
15 | })
16 |
17 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T? {
18 | return weakReference.get()
19 | }
20 |
21 | operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) {
22 | weakReference = WeakReference(value)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable/function_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
8 |
9 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DoubleD0721/Screenshot/619e11172d68f73c1eeab924be305068e383e148/app/src/main/res/drawable/icon.png
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_function_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/view_screenshot_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
15 |
16 |
21 |
22 |
--------------------------------------------------------------------------------
/app/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v23/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Screenshot
3 |
4 |
5 | Start Monitor Screenshot
6 | End Monitor Screenshot
7 | Empty Path
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
21 |
--------------------------------------------------------------------------------
/app/src/test/java/com/android/screenshot/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.screenshot
2 |
3 | import org.junit.Assert.assertEquals
4 | import org.junit.Test
5 |
6 | /**
7 | * Example local unit test, which will execute on the development machine (host).
8 | *
9 | * See [testing documentation](http://d.android.com/tools/testing).
10 | */
11 | class ExampleUnitTest {
12 | @Test
13 | fun addition_isCorrect() {
14 | assertEquals(4, 2 + 2)
15 | }
16 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.androidApplication) apply false
4 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false
5 | }
--------------------------------------------------------------------------------
/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=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.3.0"
3 | kotlin = "1.9.0"
4 | coreKtx = "1.10.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.1.5"
7 | espressoCore = "3.5.1"
8 | appcompat = "1.6.1"
9 | material = "1.10.0"
10 | constraintlayout = "2.1.4"
11 | navigationFragmentKtx = "2.6.0"
12 | navigationUiKtx = "2.6.0"
13 |
14 | [libraries]
15 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
16 | junit = { group = "junit", name = "junit", version.ref = "junit" }
17 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
18 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
19 | androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
20 | material = { group = "com.google.android.material", name = "material", version.ref = "material" }
21 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
22 | androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" }
23 | androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" }
24 |
25 | [plugins]
26 | androidApplication = { id = "com.android.application", version.ref = "agp" }
27 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
28 |
29 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DoubleD0721/Screenshot/619e11172d68f73c1eeab924be305068e383e148/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Mar 18 20:55:29 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "androidscreenshot"
23 | include(":app")
24 |
--------------------------------------------------------------------------------