This is the 12th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

scenario

Many users must have encountered the sharing function, but more advanced sharing is similar to the annual bill summary form of Alipay, which generates a screenshot of the screen increase and then shares it. Of course, I also have such demand, so how to achieve it?

First, what are the screenshot methods of Android

1. Use a DecorView to get a screenshot

Take a screenshot (assuming the Activity has already loaded) by getting a DecorView that is the top View of the entire Window, so the screenshot does not contain the status bar:

View view = getWindow().getDecorView(); / / get DecorView view. SetDrawingCacheEnabled (true); view.buildDrawingCache(); Bitmap bitmap1 = view.getDrawingCache();Copy the code

or

Bitmap bitmap2 = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888);    
Canvas canvas = new Canvas();    
canvas.setBitmap(bitmap2);    
view.draw(canvas);
Copy the code

Save bitmap1 or Bitmap2 to save screen shots

2, by calling the system source method screenshot

Since the system’s method is hide, you need to use reflection

public Bitmap takeScreenShot() {
        Bitmap bmp = null;
        mDisplay.getMetrics(mDisplayMetrics);
        float[] dims = {(float) mDisplayMetrics.widthPixels, (float) heightPixels};
        float degrees = getDegreesForRotation(mDisplay.getRotation());
        boolean requiresRotation = degrees > 0;
        if (requiresRotation) {
            mDisplayMatrix.reset();
            mDisplayMatrix.preRotate(-degrees);
            mDisplayMatrix.mapPoints(dims);
            dims[0] = Math.abs(dims[0]);
            dims[1] = Math.abs(dims[1]);
        }
        try {
            Class<?> demo = Class.forName("android.view.SurfaceControl");
            Method method = demo.getMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE});
            bmp = (Bitmap) method.invoke(demo, new Object[]{Integer.valueOf((int) dims[0]), Integer.valueOf((int) dims[1])});
            if (bmp == null) {
                return null;
            }
            if (requiresRotation) {
                Bitmap ss = Bitmap.createBitmap(mDisplayMetrics.widthPixels, heightPixels, Bitmap.Config.RGB_565);
                Canvas c = new Canvas(ss);
                c.translate((float) (ss.getWidth() / 2), (float) (ss.getHeight() / 2));
                c.rotate(degrees);
                c.translate((-dims[0] / 2), (-dims[1] / 2));
                c.drawBitmap(bmp, 0, 0, null);
                c.setBitmap(null);
                bmp.recycle();
                bmp = ss;
            }
            if (bmp == null) {
                return null;
            }
            bmp.setHasAlpha(false);
            bmp.prepareToDraw();
            return bmp;
        } catch (Exception e) {
            e.printStackTrace();
            return bmp;
        }
    }

Copy the code

Ii. Monitoring of mobile phone screenshots

Use FileObserver to monitor resource changes in a directory. Use ContentObserver to monitor changes in all resources; ContentObserver: Monitors image multimedia changes through ContentObserver. When a new image file is generated on your phone, a record is inserted into the image database through the MediaProvider class, listening for image insertion events to obtain the URI of the image. Today we are doing it through ContentObserver;

1)ScreenShotHelper help class

/** * Description: */ ScreenShotHelper {companion Object {const val TAG = "ScreenShotLog" /** ** The column that needs to be read when reading the media database */ val MEDIA_PROJECTIONS = arrayOf( MediaStore.Images.ImageColumns.DATA, MediaStore. Images. ImageColumns. DATE_TAKEN) / * * * need to read while reading media database columns, The width and height fields after API 16 * / val MEDIA_PROJECTIONS_API_16 = arrayOf (MediaStore. Images. ImageColumns. DATA, MediaStore.Images.ImageColumns.DATE_TAKEN, MediaStore.Images.ImageColumns.WIDTH, MediaStore. Images. ImageColumns. HEIGHT) / * * * the screenshots path judgment keyword * / val KEYWORDS = arrayOf (" screenshot ", "screen_shot", "screen-shot", "screen shot", "screencapture", "screen_capture", "screen-capture", "screen capture", "screencap", "screen_cap", "screen-cap", "screen cap" ) fun showLog(msg: String) { Log.d(TAG, msg) } } }Copy the code

2)Listener ScreenShotListener

/**
 * Description: 截屏监听
 */
class ScreenShotListener constructor(context: Context?) {
    private var mContext: Context
    private var mScreenRealSize: Point? = null
    private val mHasCallbackPaths: ArrayList<String> = ArrayList()
    private var mListener: OnScreenShotListener? = null
    private var mStartListenTime: Long = 0
    /**
     * 内部存储器内容观察者
     */
    private var mInternalObserver: MediaContentObserver? = null
    /**
     * 外部存储器内容观察者
     */
    private var mExternalObserver: MediaContentObserver? = null
    /**
     * 运行在 UI 线程的 Handler, 用于运行监听器回调
     */
    private var mUiHandler = Handler(Looper.getMainLooper())
    init {
        ScreenShotHelper.showLog("init")
        assertInMainThread()
        requireNotNull(context) { "The context must not be null." }
        mContext = context
        if (mScreenRealSize == null) {
            mScreenRealSize = getRealScreenSize()
            if (mScreenRealSize != null) {
                ScreenShotHelper.showLog("Screen Real Size: " + mScreenRealSize!!.x + " * " + mScreenRealSize!!.y)
            } else {
                ScreenShotHelper.showLog("Get screen real size failed.")
            }
        }
    }
    /**
     * 单例
     */
    companion object : SingletonHolder<ScreenShotListener, Context>(::ScreenShotListener)
    /**
     * 开启监听
     */
    fun startListener() {
        assertInMainThread()
        // 记录开始监听的时间戳
        mStartListenTime = System.currentTimeMillis()
        // 创建内容观察者
        mInternalObserver =
            MediaContentObserver(MediaStore.Images.Media.INTERNAL_CONTENT_URI, mUiHandler)
        mExternalObserver =
            MediaContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, mUiHandler)
        // 注册内容观察者
        mContext.contentResolver.registerContentObserver(
            MediaStore.Images.Media.INTERNAL_CONTENT_URI,
            false,
            mInternalObserver
        )
        mContext.contentResolver.registerContentObserver(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            false,
            mExternalObserver
        )
    }
    fun stopListener() {
        assertInMainThread()
        // 注销内容观察者
        if (mInternalObserver != null) {
            try {
                mContext.contentResolver.unregisterContentObserver(mInternalObserver!!)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            mInternalObserver = null
        }
        if (mExternalObserver != null) {
            try {
                mContext.contentResolver.unregisterContentObserver(mExternalObserver!!)
            } catch (e: Exception) {
                e.printStackTrace()
            }
            mExternalObserver = null
        }
        // 清空数据
        mStartListenTime = 0
        mListener = null
    }
    /**
     * 处理媒体数据库的内容改变
     */
    fun handleMediaContentChange(contentUri: Uri) {
        var cursor: Cursor? = null
        try {
            cursor = mContext.contentResolver.query(
                contentUri,
                if (Build.VERSION.SDK_INT < 16) ScreenShotHelper.MEDIA_PROJECTIONS else ScreenShotHelper.MEDIA_PROJECTIONS_API_16,
                null, null,
                "${MediaStore.Images.ImageColumns.DATE_ADDED} desc limit 1"
            )
            if (cursor == null) {
                ScreenShotHelper.showLog("Deviant logic.")
                return
            }
            if (!cursor.moveToFirst()) {
                ScreenShotHelper.showLog("Cursor no data.")
                return
            }
            // 获取各列的索引
            val dataIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)
            val dateTakenIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATE_TAKEN)
            var widthIndex = -1
            var heightIndex = -1
            if (Build.VERSION.SDK_INT >= 16) {
                widthIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.WIDTH)
                heightIndex = cursor.getColumnIndex(MediaStore.Images.ImageColumns.HEIGHT)
            }
            // 获取行数据
            val data = cursor.getString(dataIndex)
            val dateTaken = cursor.getLong(dateTakenIndex)
            var width = 0
            var height = 0
            if (widthIndex >= 0 && heightIndex >= 0) {
                width = cursor.getInt(widthIndex)
                height = cursor.getInt(heightIndex)
            } else {
                val size = getImageSize(data)
                width = size.x
                height = size.y
            }
            // 处理获取到的第一行数据
            handleMediaRowData(data, dateTaken, width, height)
        } catch (e: Exception) {
            ScreenShotHelper.showLog("Exception: ${e.message}")
            e.printStackTrace()
        } finally {
            if (cursor != null && !cursor.isClosed) {
                cursor.close()
            }
        }
    }
    private fun getImageSize(imagePath: String): Point {
        val options = BitmapFactory.Options()
        options.inJustDecodeBounds = true
        BitmapFactory.decodeFile(imagePath, options)
        return Point(options.outWidth, options.outHeight)
    }
    /**
     * 处理获取到的一行数据
     */
    private fun handleMediaRowData(data: String, dateTaken: Long, width: Int, height: Int) {
        if (checkScreenShot(data, dateTaken, width, height)) {
            ScreenShotHelper.showLog("ScreenShot: path = $data; size = $width * $height; date = $dateTaken")
            if (mListener != null && !checkCallback(data)) {
                mListener!!.onScreenShot(data)
            }
        } else {
            // 如果在观察区间媒体数据库有数据改变,又不符合截屏规则,则输出到 log 待分析
            ScreenShotHelper.showLog("Media content changed, but not screenshot: path = $data; size = $width * $height; date = $dateTaken")
        }
    }
    /**
     * 判断指定的数据行是否符合截屏条件
     */
    private fun checkScreenShot(data: String?, dateTaken: Long, width: Int, height: Int): Boolean {
        // 判断依据一: 时间判断
        // 如果加入数据库的时间在开始监听之前, 或者与当前时间相差大于10秒, 则认为当前没有截屏
        if (dateTaken < mStartListenTime || System.currentTimeMillis() - dateTaken > 10 * 1000) {
            return false
        }
        // 判断依据二: 尺寸判断
        if (mScreenRealSize != null) {
            // 如果图片尺寸超出屏幕, 则认为当前没有截屏
            if (!(width <= mScreenRealSize!!.x && height <= mScreenRealSize!!.y)
                || (height <= mScreenRealSize!!.x && width <= mScreenRealSize!!.y)
            ) {
                return false
            }
        }
        // 判断依据三: 路径判断
        if (data.isNullOrEmpty()) {
            return false
        }
        val lowerData = data.toLowerCase(Locale.getDefault())
        // 判断图片路径是否含有指定的关键字之一, 如果有, 则认为当前截屏了
        for (keyWork in ScreenShotHelper.KEYWORDS) {
            if (lowerData.contains(keyWork)) {
                return true
            }
        }
        return false
    }
    /**
     * 判断是否已回调过, 某些手机ROM截屏一次会发出多次内容改变的通知; <br></br>
     * 删除一个图片也会发通知, 同时防止删除图片时误将上一张符合截屏规则的图片当做是当前截屏.
     */
    private fun checkCallback(imagePath: String): Boolean {
        if (mHasCallbackPaths.contains(imagePath)) {
            ScreenShotHelper.showLog("ScreenShot: imgPath has done; imagePath = $imagePath")
            return true
        }
        // 大概缓存15~20条记录便可
        if (mHasCallbackPaths.size >= 20) {
            for (i in 0..4) {
                mHasCallbackPaths.removeAt(0)
            }
        }
        mHasCallbackPaths.add(imagePath)
        return false
    }
    /**
     * 获取屏幕分辨率
     */
    private fun getRealScreenSize(): Point? {
        var screenSize: Point? = null
        try {
            screenSize = Point()
            val windowManager = mContext.getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val defaultDisplay = windowManager.defaultDisplay
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                defaultDisplay.getRealSize(screenSize)
            } else {
                try {
                    val mGetRawW = Display::class.java.getMethod("getRawWidth")
                    val mGetRawH = Display::class.java.getMethod("getRawHeight")
                    screenSize.set(
                        mGetRawW.invoke(defaultDisplay) as Int,
                        mGetRawH.invoke(defaultDisplay) as Int
                    )
                } catch (e: Exception) {
                    screenSize.set(defaultDisplay.width, defaultDisplay.height)
                    e.printStackTrace()
                }
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return screenSize
    }
    private fun assertInMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            val stackTrace = Thread.currentThread().stackTrace
            var methodMsg: String? = null
            if (stackTrace != null && stackTrace.size >= 4) {
                methodMsg = stackTrace[3].toString()
            }
            ScreenShotHelper.showLog("Call the method must be in main thread: $methodMsg")
        }
    }
    /**
     * 媒体内容观察者
     */
    private inner class MediaContentObserver(var contentUri: Uri, handler: Handler) :
        ContentObserver(handler) {
        override fun onChange(selfChange: Boolean) {
            super.onChange(selfChange)
            handleMediaContentChange(contentUri)
        }
    }
    /**
     * 设置截屏监听器回调
     */
    fun setListener(listener: OnScreenShotListener) {
        this.mListener = listener
    }
    /**
     * 截屏监听接口
     */
    interface OnScreenShotListener {
        fun onScreenShot(picPath: String)
    }
}
Copy the code

3) Method of use

class ScreenShotActivity : AppCompatActivity() { private lateinit var screenShotListener: ScreenShotListener var isHasScreenShotListener = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_screen_shot) screenShotListener = ScreenShotListener. GetInstance (this) / / apply for permission to val permission = arrayOf (Manifest. Permission. READ_EXTERNAL_STORAGE) the if (ActivityCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE ) ! = PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions(this, permission, 1001) } } override fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == 1001) { if (grantResults[0] == PermissionChecker.PERMISSION_GRANTED) { Override fun onResume() {super.onResume() override fun onResume() {super.onResume() startScreenShotListen() } override fun onPause() { super.onPause() stopScreenShotListen() } private fun startScreenShotListen() { if (! isHasScreenShotListener) { screenShotListener.setListener(object : ScreenShotListener.OnScreenShotListener { override fun onScreenShot(picPath: Log.d(screenshothelper. TAG, picPath) } }) screenShotListener.startListener() isHasScreenShotListener = true } } private fun stopScreenShotListen() {  if (isHasScreenShotListener) { screenShotListener.stopListener() isHasScreenShotListener = false } } }Copy the code

Note the following points:

1. If you want to listen for all page-level screenshots in your project, it is best to put the listener in your project BaseActivity, then enable the listener in onResume and disable the listener in onPause.

2. You need to add the storage permission for screenshots (internal storage (READ_EXTERNAL_STORAGE)).

conclusion

The screenshots I’ve covered so far are only for screenshots at the current page level, as well as scrolling lists, which are long screenshots, which I’ll cover later.