├── .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 | --------------------------------------------------------------------------------