-
Notifications
You must be signed in to change notification settings - Fork 151
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #189 from GuoXiCheng/dev-c
add LayoutInspectService
- Loading branch information
Showing
7 changed files
with
254 additions
and
51 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 62 additions & 43 deletions
105
app/src/main/java/com/android/skip/LayoutInspectActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
164
app/src/main/java/com/android/skip/service/LayoutInspectService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters