/* * * * Copyright © 2023-2025 by Navi Technologies Limited * * All rights reserved. Strictly confidential * */ package com.navi.alfred.utils import android.app.Activity import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.os.Build import android.view.View import android.view.ViewGroup import android.view.Window import androidx.core.view.ViewCompat import androidx.core.view.isVisible import com.navi.alfred.AlfredManager import com.navi.alfred.db.AlfredDatabaseHelper import com.navi.alfred.db.model.ScreenShotPathHelper import java.io.File import java.io.FileOutputStream import java.io.IOException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch internal suspend fun handleScreenShot() { if ( ScreenShotStorageHelper.images.size == AlfredManager.imageThreshHoldValue || ScreenShotStorageHelper.images.size == AlfredManager.imageSecondThreshHoldValue ) { toZip(AlfredManager.screenShotDao.fetchScreenShotsPath(AlfredManager.imageThreshHoldValue)) } else { if (ScreenShotStorageHelper.images.size == 1) { checkToStartZipUpload() } } } internal fun combineScreenshots( backgroundScreenshot: Bitmap?, bottomSheetScreenshot: Bitmap?, context: Context, screenName: String? = null, moduleName: String? = null, scope: CoroutineScope, view: View, ): Bitmap? { if ( backgroundScreenshot == null || bottomSheetScreenshot == null || isScreenDisabled(screenName, moduleName) ) { return null } var screenWidth = view.width / 2 var screenHeight = view.height / 2 if (!isResolutionEven(screenWidth, screenHeight)) { screenHeight += 1 screenWidth += 1 } val canvas = Canvas(backgroundScreenshot) var bottomSheetLeft = (screenWidth - bottomSheetScreenshot.width) / 2f var bottomSheetTop = screenHeight - bottomSheetScreenshot.height.toFloat() if (!isResolutionEven(bottomSheetLeft.toInt(), bottomSheetTop.toInt())) { bottomSheetLeft = (bottomSheetLeft.toInt() + 1).toFloat() bottomSheetTop = (bottomSheetTop.toInt() + 1).toFloat() } canvas.drawBitmap(bottomSheetScreenshot, bottomSheetLeft, bottomSheetTop, null) insertScreenShotPathInDb(scope, context, backgroundScreenshot) return backgroundScreenshot } internal fun insertScreenShotPathInDb( scope: CoroutineScope, context: Context, bitmap: Bitmap?, bottomSheetFlow: Boolean? = false, ) { scope.launch(Dispatchers.IO) { try { val fileDir = context.filesDir val currentTime = AlfredManager.config.getAlfredCurrentTimeMillis() AlfredManager.config.setLatestScreenshotTimestamp(currentTime) val imageFileExtension = if (AlfredManager.config.imageType == AlfredConstants.IMAGE_TYPE_WEBP) { AlfredConstants.IMAGE_FILE_EXTENSION_WEBP } else { AlfredConstants.IMAGE_FILE_EXTENSION_JPEG } val fileName = currentTime.toString() + imageFileExtension val path = fileDir.path.plus("/").plus(fileName) val imageUrl = File(path) val fos = FileOutputStream(imageUrl) if (AlfredManager.config.imageType == AlfredConstants.IMAGE_TYPE_WEBP) { bitmap?.compress(Bitmap.CompressFormat.WEBP, 0, fos) } else { val videoQuality: Int = when (AlfredManager.config.getVideoQuality()) { VideoQuality.HIGH.name -> { 10 } VideoQuality.LOW.name -> { 6 } VideoQuality.MEDIUM.name -> { 8 } else -> { 10 } } bitmap?.compress(Bitmap.CompressFormat.JPEG, videoQuality, fos) } fos.flush() fos.close() if (bottomSheetFlow == false) { val db = AlfredDatabaseHelper.getAnalyticsDatabase(context) val screenShotDao = db.screenShotDao() try { screenShotDao.insertScreenShotPath( ScreenShotPathHelper( AlfredManager.config.getAlfredCurrentTimeMillis(), path, ) ) ScreenShotStorageHelper.addImage(path) } catch (e: Exception) { e.log() } } } catch (e: Exception) { e.log() } catch (e: IOException) { e.log() } } } internal fun captureScreenshotOfCustomView(view: View): Bitmap? { view.draw(Canvas()) val bitmapForCanvas = createBitmapForView(view) try { bitmapForCanvas?.first?.let { view.draw(it) } } catch (e: Exception) { e.log() } return bitmapForCanvas?.second } internal fun deleteScreenShot() { try { val screenShotPathList: List = if ( AlfredManager.screenShotDao.getScreenShotCount() >= AlfredManager.imageThreshHoldValue ) { AlfredManager.screenShotDao.fetchScreenShotsPath(AlfredManager.imageThreshHoldValue) } else { AlfredManager.screenShotDao.fetchAllScreenShotsPath() } clearScreenShot(screenShotPathList) try { if (AlfredManager.isAppInBackground) { ScreenShotStorageHelper.clearAll() AlfredManager.screenShotDao.deleteAllScreenShot() AlfredManager.isAppInBackground = false AlfredManager.hasRecordingStarted = false } else { val idList = screenShotPathList.map { it.id } AlfredManager.screenShotDao.deleteScreenShot(idList) ScreenShotStorageHelper.deleteKItems(idList.size) } } catch (e: Exception) { e.log() } } catch (e: Exception) { e.log() } } internal fun clearScreenShot(path: List) { path.forEach { data -> val file = data.screenShotPath?.let { File(it) } if (file?.exists() == true) { file.delete() } } } enum class VideoQuality { LOW, HIGH, MEDIUM, } internal suspend fun captureScreen( v: View, screenName: String? = null, canvas: Canvas? = null, bmp: Bitmap? = null, moduleName: String? = null, activity: Activity? = null, ): Bitmap? { if ( isScreenDisabled(screenName, moduleName) || isScreenDisabled(AlfredManager.currentScreenName, moduleName) || canvas == null || bmp == null || activity == null ) { return null } AlfredManager.coroutineScope.launch { if (isMaskingEnabled(screenName) || isMaskingEnabled(AlfredManager.currentScreenName)) { try { val rootView = AlfredManager.sensitiveComposeRepository.getRootViewOfComposeScreen() if (rootView != null) { if (AlfredManager.sensitiveComposeRepository.getBlurSensitiveScreenStatus()) { blurScreen(canvas, rootView) } else { captureScreenManager( view = rootView, bitmap = bmp, canvas = canvas, shouldMaskComposeScreen = true, activity = activity, ) } } else { if (v.tag == AlfredConstants.SENSITIVE_VIEW_TAG) { captureScreenManager( view = v, bitmap = bmp, canvas = canvas, shouldMaskXmlScreen = true, activity = activity, ) } else { captureScreenManager( view = v, bitmap = bmp, canvas = canvas, activity = activity, ) } } } catch (e: Exception) { e.log() } } else { captureScreenManager(view = v, bitmap = bmp, canvas = canvas, activity = activity) } } return bmp } internal suspend fun captureScreenManager( view: View, bitmap: Bitmap, canvas: Canvas, shouldMaskComposeScreen: Boolean? = false, shouldMaskXmlScreen: Boolean? = false, activity: Activity?, ) { try { if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && AlfredManager.config.getPixelCopyStatus() ) { getScreenShotUsingPixelCopy( view = view, bitmap = bitmap, activity = activity, canvas = canvas, shouldMaskComposeScreen = shouldMaskComposeScreen, shouldMaskXmlScreen = shouldMaskXmlScreen, ) } else { getScreenShotUsingCanvasDraw( view = view, bitmap = bitmap, canvas = canvas, shouldMaskComposeScreen = shouldMaskComposeScreen, shouldMaskXmlScreen = shouldMaskXmlScreen, ) } } catch (e: Exception) { e.log() } } internal fun blurScreen(canvas: Canvas, rootView: View) { val paint = Paint() paint.color = Color.GRAY paint.alpha = 240 canvas.drawRect( rootView.left.toFloat(), rootView.top.toFloat(), rootView.right.toFloat(), rootView.bottom.toFloat(), paint, ) } @JvmOverloads internal fun measureInflatedView(view: View, width: Int = 1080, height: Int = 1920) { view.layoutParams = ViewGroup.LayoutParams(width, height) view.measure( View.MeasureSpec.makeMeasureSpec(view.layoutParams.width, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(view.layoutParams.height, View.MeasureSpec.EXACTLY), ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) } internal fun createBitmapForView(view: View): Pair? { var width = view.width / 2 var height = view.height / 2 if (width > 0 && height > 0) { if (!isResolutionEven(width, height)) { width += 1 height += 1 } val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) val canvas = Canvas(bitmap) canvas.scale(0.5f, 0.5f) return Pair(canvas, bitmap) } return null } internal fun isResolutionEven(width: Int, height: Int): Boolean { return width.mod(2) == 0 && height.mod(2) == 0 } internal fun isViewCaptureReady(activity: Activity?, view: View): Boolean { return activity != null && view.isVisible && view.isAttachedToWindow && view.width != 0 && view.height != 0 && !activity.isFinishing && !activity.isDestroyed } internal fun isWindowCaptureReady(window: Window?): Boolean { return window != null && window.decorView.width != 0 && window.decorView.height != 0 && window.peekDecorView() != null && window.isActive && ViewCompat.isLaidOut(window.decorView) }