/* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.eyedropper import android.content.Intent import android.graphics.Bitmap import android.hardware.display.DisplayManager import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope import com.android.eyedropper.desktop.DesktopControllerService import com.android.eyedropper.desktop.DesktopServiceConnection import com.android.eyedropper.touchscreen.TouchscreenControllerActivity import com.android.eyedropper.util.InputHelper import com.android.eyedropper.util.ScreenCaptureHelper import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async private const val TAG = "MainActivity" enum class Mode { DESKTOP, TOUCHSCREEN } /** * The entry point of request that delegates to one of the UX implementations. */ @AndroidEntryPoint(ComponentActivity::class) class MainActivity : Hilt_MainActivity() { @Inject lateinit var inputHelper: InputHelper @Inject lateinit var screenCaptureHelper: ScreenCaptureHelper @Inject lateinit var displayManager: DisplayManager private var desktopConnection: DesktopServiceConnection? = null // TODO: b/399172476 - Refactor to make MainActivity not need to know which UI impl it's using private lateinit var mode: Mode // Launch touchscreen UX via startActivityForResult. // Set as val because launchers must be set unconditionally private val touchscreenActivityLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == RESULT_OK) { val colorInt = result.data?.getIntExtra(Intent.EXTRA_COLOR, -1)!! sendColor(colorInt) } else { Log.d(TAG, "resultCode != OK") finish() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d(TAG, "onCreate called") if (inputHelper.isDesktopMode()) { mode = Mode.DESKTOP // Set once; mode can't change during request, otherwise would abort // Create Desktop UI service if doesn't exist, then bind // Launch screenshot job as early as possible val displayIds = displayManager.displays.map { it.displayId } val deferredScreenshotsMap: Map> = displayIds.associateWith { lifecycleScope.async(Dispatchers.IO) { screenCaptureHelper.captureScreenshotAsync(it) } } desktopConnection = DesktopServiceConnection( deferredScreenshotsMap, ::sendColor, ::onAbort ) } else { mode = Mode.TOUCHSCREEN val intent = Intent(this, TouchscreenControllerActivity::class.java) // Start activity only if initial launch if (savedInstanceState == null) { touchscreenActivityLauncher.launch(intent) } } } override fun onStart() { super.onStart() Log.d(TAG, "onStart called") when (mode) { Mode.DESKTOP -> { // Create Desktop UI service if doesn't exist, then bind val connection = checkNotNull(desktopConnection) val intent = Intent(this, DesktopControllerService::class.java) if (!bindService(intent, connection, BIND_AUTO_CREATE)) { Log.e(TAG, "Failed to bind to DesktopControllerService") finish() } } Mode.TOUCHSCREEN -> {} } } override fun onStop() { Log.d(TAG, "onStop called") super.onStop() when (mode) { Mode.DESKTOP -> { val connection = checkNotNull(desktopConnection) if (!connection.isBound) return try { unbindService(connection) connection.isBound = false } catch (e: IllegalArgumentException) { Log.e(TAG, "Error unbinding service: $e") } catch (e: Exception) { Log.d(TAG, "Error on onStop teardown: $e") } } Mode.TOUCHSCREEN -> {} } } /** * Callback passed to the UI. The UI layer records the click coordinate, and this function * retrieves the color at the coordinate. */ private fun sendColor(colorInt: Int) { Log.d(TAG, "sendColor called! sending over $colorInt") val resultIntent = Intent().apply { putExtra(Intent.EXTRA_COLOR, colorInt) } setResult(RESULT_OK, resultIntent) finish() } /** * Callback passed to the UI. The logic to be executed on some form of disruption to * color selection flow, input device change or manual abort via escape key. */ private fun onAbort() { Log.d(TAG, "onAbort called.") setResult(RESULT_CANCELED) finish() } }