Skip to content

Commit

Permalink
Merge pull request #189 from GuoXiCheng/dev-c
Browse files Browse the repository at this point in the history
add LayoutInspectService
  • Loading branch information
GuoXiCheng authored Aug 6, 2024
2 parents 0602988 + 5a9d89c commit 2e4b04e
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 51 deletions.
7 changes: 7 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

<application
android:allowBackup="true"
Expand Down Expand Up @@ -72,6 +73,12 @@
android:name="android.accessibilityservice"
android:resource="@xml/accessibility" />
</service>

<service
android:name=".service.LayoutInspectService"
android:enabled="true"
android:foregroundServiceType="mediaProjection"
/>
</application>

</manifest>
4 changes: 1 addition & 3 deletions app/src/main/java/com/android/skip/BaseActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@ const val SKIP_PERMIT_NOTICE = "SKIP_PERMIT_NOTICE"

const val SKIP_INCLUDE_SYSTEM_APPS = "SKIP_INCLUDE_SYSTEM_APPS"

const val SKIP_FLOATING_WINDOW = "SKIP_FLOATING_WINDOW"

const val SKIP_SCREEN_CAPTURE = "SKIP_SCREEN_CAPTURE"
const val SKIP_LAYOUT_INSPECT = "SKIP_LAYOUT_INSPECT"

val themeTypeState: MutableState<Int> by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
mutableStateOf(DataStoreUtils.getSyncData(SKIP_APP_THEME, Configuration.UI_MODE_NIGHT_NO))
Expand Down
105 changes: 62 additions & 43 deletions app/src/main/java/com/android/skip/LayoutInspectActivity.kt
Original file line number Diff line number Diff line change
@@ -1,69 +1,88 @@
package com.android.skip

import android.app.Activity
import android.content.Intent
import android.media.projection.MediaProjectionManager
import android.os.Bundle
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import com.android.skip.compose.FlatButton
import com.android.skip.compose.ResourceIcon
import com.android.skip.compose.RowContent
import com.android.skip.compose.ScaffoldPage
import com.android.skip.service.LayoutInspectService
import com.android.skip.utils.DataStoreUtils

class LayoutInspectActivity : BaseActivity() {
private lateinit var screenshotPermissionLauncher: ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

screenshotPermissionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val intent = Intent(this, LayoutInspectService::class.java).apply {
putExtra("resultCode", result.resultCode)
putExtra("data", result.data)
}
ContextCompat.startForegroundService(this, intent)
}
}
}

@Composable
override fun ProvideContent() {
LayoutInspectInterface {
finish()
}

}
}

@Composable
fun LayoutInspectInterface(onBackClick: () -> Unit) {
@Composable
fun LayoutInspectInterface(onBackClick: () -> Unit) {
val context = LocalContext.current

val checkFloatingWindow = remember {
mutableStateOf(
DataStoreUtils.getSyncData(
SKIP_FLOATING_WINDOW, false
val checkLayoutInspect = remember {
mutableStateOf(
DataStoreUtils.getSyncData(
SKIP_LAYOUT_INSPECT, false
)
)
)
}
}

val checkScreenCapture = remember {
mutableStateOf(
DataStoreUtils.getSyncData(
SKIP_SCREEN_CAPTURE, false
)
)
}
ScaffoldPage(
stringResource(id = R.string.layout_inspect),
onBackClick = onBackClick,
content = {
FlatButton(content = {
RowContent(stringResource(id = R.string.layout_inspect_title),
stringResource(id = R.string.layout_inspect_subtitle),
{ ResourceIcon(iconResource = R.drawable.fit_screen) },
checkLayoutInspect.value,
{
checkLayoutInspect.value = it
DataStoreUtils.putSyncData(SKIP_LAYOUT_INSPECT, it)
if (checkLayoutInspect.value) {
val mediaProjectionManager =
getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager

ScaffoldPage(stringResource(id = R.string.layout_inspect), onBackClick = onBackClick, content = {
FlatButton(content = {
RowContent(
stringResource(id = R.string.inspect_screen_capture_title),
stringResource(id = R.string.inspect_screen_capture_subtitle),
{ ResourceIcon(iconResource = R.drawable.screenshot_keyboard) },
checkScreenCapture.value,
{
checkScreenCapture.value = it
DataStoreUtils.putSyncData(SKIP_SCREEN_CAPTURE, it)
}
)
})
val captureIntent =
mediaProjectionManager.createScreenCaptureIntent()
screenshotPermissionLauncher.launch(captureIntent)
} else {
val intent = Intent(context, LayoutInspectService::class.java)
stopService(intent)
}
})
})
})
}
}

FlatButton(content = {
RowContent(
stringResource(id = R.string.inspect_floating_window_title),
stringResource(id = R.string.inspect_floating_window_subtitle),
{ ResourceIcon(iconResource = R.drawable.float_landscape_2) },
checkFloatingWindow.value,
{
checkFloatingWindow.value = it
DataStoreUtils.putSyncData(SKIP_FLOATING_WINDOW, it)
}
)
})
})
}
164 changes: 164 additions & 0 deletions app/src/main/java/com/android/skip/service/LayoutInspectService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.android.skip.service

import android.annotation.SuppressLint
import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.PixelFormat
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.ImageReader
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.DisplayMetrics
import android.view.KeyEvent
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.android.skip.NewMainActivity
import com.android.skip.R
import com.android.skip.SKIP_LAYOUT_INSPECT
import com.android.skip.manager.ToastManager
import com.android.skip.utils.DataStoreUtils
import java.io.File
import java.io.FileOutputStream
import java.io.IOException


class LayoutInspectService: Service() {
private var mMediaProjection: MediaProjection? = null
private var mProjectionManager:MediaProjectionManager? = null
private var virtualDisplay: VirtualDisplay? = null
private var isProcessingImage = false

override fun onBind(p0: Intent?): IBinder? {
TODO("Not yet implemented")
}

override fun onCreate() {
super.onCreate()

// 开启前台服务
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel("layout_inspect_service", "前台布局检查服务通知", NotificationManager.IMPORTANCE_DEFAULT)
manager.createNotificationChannel(channel)
}
val it = Intent(this, NewMainActivity::class.java)
val pi = PendingIntent.getActivity(this, 0, it, PendingIntent.FLAG_IMMUTABLE)
val notification = NotificationCompat.Builder(this, "layout_inspect_service")
.setContentTitle("布局检查服务已准备就绪")
.setContentText("布局检查服务将在运行一次后退出")
.setSmallIcon(R.drawable.warning)
.setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.warning))
.setContentIntent(pi)
.build()
startForeground(1, notification)
}

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val keyCode = intent?.getIntExtra("keyCode", -1)

if (mMediaProjection == null) {
val resultCode = intent?.getIntExtra("resultCode", Activity.RESULT_CANCELED)
val data = intent?.getParcelableExtra("data", Intent::class.java)
if (resultCode == Activity.RESULT_OK && data != null) {
mProjectionManager = getSystemService(MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
mMediaProjection = mProjectionManager?.getMediaProjection(resultCode, data)
}
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
isProcessingImage = true
setupVirtualDisplay()
}

return START_NOT_STICKY
}

override fun onDestroy() {
super.onDestroy()
mMediaProjection?.stop()
DataStoreUtils.putSyncData(SKIP_LAYOUT_INSPECT, false)
}

@SuppressLint("WrongConstant")
private fun setupVirtualDisplay() {
val metrics = DisplayMetrics()
val windowManager = applicationContext.getSystemService(Activity.WINDOW_SERVICE) as android.view.WindowManager
windowManager.defaultDisplay.getMetrics(metrics)
val density = metrics.densityDpi

val displayWidth = metrics.widthPixels
val displayHeight = metrics.heightPixels

val imageReader = ImageReader.newInstance(displayWidth, displayHeight, PixelFormat.RGBA_8888, 2)
virtualDisplay = mMediaProjection?.createVirtualDisplay(
"ScreenCapture",
displayWidth, displayHeight, density,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
imageReader.surface, null, null
)

imageReader.setOnImageAvailableListener({reader ->
if (!isProcessingImage) return@setOnImageAvailableListener

val image = reader.acquireLatestImage()
if (image != null) {
val planes = image.planes
val buffer = planes[0].buffer
val pixelStride = planes[0].pixelStride
val rowStride = planes[0].rowStride
val rowPadding = rowStride - pixelStride * displayWidth

// Create Bitmap
val bitmap = Bitmap.createBitmap(
displayWidth + rowPadding / pixelStride,
displayHeight,
Bitmap.Config.ARGB_8888
)
bitmap.copyPixelsFromBuffer(buffer)
image.close()

// 保存或处理bitmap
val file = getOutputFile()
val success = saveBitmapToFile(bitmap, file)
if (success) {
ToastManager.showToast(this, "保存成功")
} else {
ToastManager.showToast(this, "保存失败")
}

isProcessingImage = false
stopSelf()
}
}, Handler(Looper.getMainLooper()))
}

private fun getOutputFile(): File {
val picturesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
return File(picturesDir, "screenshot_${System.currentTimeMillis()}.png")
}

private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
return try {
val outputStream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package com.android.skip.service

import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.GestureDescription
import android.content.Intent
import android.graphics.Path
import android.graphics.Rect
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.android.skip.SKIP_LAYOUT_INSPECT
import com.android.skip.SKIP_PERMIT_NOTICE
import com.android.skip.handler.BoundsHandler
import com.android.skip.handler.IdNodeHandler
Expand Down Expand Up @@ -79,4 +82,17 @@ class MyAccessibilityService : AccessibilityService() {
null
)
}

override fun onKeyEvent(event: KeyEvent?): Boolean {
if (event != null
&& event.action == KeyEvent.ACTION_DOWN
&& event.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
&& DataStoreUtils.getSyncData(SKIP_LAYOUT_INSPECT, false)) {
val intent = Intent(this, LayoutInspectService::class.java)
intent.putExtra("keyCode", event.keyCode)
startService(intent)
return true
}
return super.onKeyEvent(event)
}
}
6 changes: 2 additions & 4 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,7 @@
<string name="dialog_open_browser_content">是否要通过浏览器访问</string>

<string name="layout_inspect">布局检查</string>
<string name="inspect_screen_capture_title">截屏服务</string>
<string name="inspect_screen_capture_subtitle">启用截屏服务以便保存屏幕布局图片</string>
<string name="inspect_floating_window_title">悬浮窗服务</string>
<string name="inspect_floating_window_subtitle">启用悬浮窗以便主动保存布局检查信息</string>
<string name="layout_inspect_title">布局检查服务</string>
<string name="layout_inspect_subtitle">启用布局检查以便保存布局图片和节点信息</string>

</resources>
3 changes: 2 additions & 1 deletion app/src/main/res/xml/accessibility.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
android:canPerformGestures="true"
android:accessibilityFeedbackType="feedbackAllMask"
android:canRetrieveWindowContent="true"
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagDefault|flagRetrieveInteractiveWindows"
android:canRequestFilterKeyEvents="true"
android:accessibilityFlags="flagReportViewIds|flagIncludeNotImportantViews|flagDefault|flagRetrieveInteractiveWindows|flagRequestFilterKeyEvents"
android:notificationTimeout="100"/>

0 comments on commit 2e4b04e

Please sign in to comment.