/* * 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.desktop import android.content.Intent import android.graphics.Bitmap import android.hardware.display.DisplayManager import android.os.Handler import android.os.Looper import android.view.Display import android.view.View import android.view.WindowManager import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.test.junit4.createEmptyComposeRule import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ServiceTestRule import com.android.eyedropper.di.MainThreadHandlerModule import com.android.eyedropper.util.InputHelper import com.android.eyedropper.util.ScreenCaptureHelper import com.android.eyedropper.util.WindowHelper import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.BindValue import dagger.hilt.android.testing.HiltAndroidRule import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.UninstallModules import java.util.concurrent.TimeoutException import kotlinx.coroutines.CompletableDeferred import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.stub import org.mockito.kotlin.verify /** * Test class for [DesktopControllerService] * * Usage: atest EyeDropperUnitTests:DesktopControllerTest */ @UninstallModules(MainThreadHandlerModule::class) @HiltAndroidTest @RunWith(AndroidJUnit4::class) class DesktopControllerServiceTest { @get:Rule(order = 0) val hiltRule = HiltAndroidRule(this) @get:Rule(order = 1) val serviceRule: ServiceTestRule = ServiceTestRule() // Will be incorrectly attached to Activity by default if not empty @get:Rule(order = 2) val composeTestRule = createEmptyComposeRule() private val displayId = Display.DEFAULT_DISPLAY @BindValue val mockInputHelper = mock { on { isDesktopMode() } doReturn true on { getCursorPosition(any()) } doReturn Pair(displayId, Offset(10f, 10f)) } @BindValue val mockWindowHelper = mock() @BindValue val mockScreenCaptureHelper = mock() @BindValue val mockInputDeviceListener = mock() @BindValue val mainThreadHandler = Handler(Looper.getMainLooper()) private val context = InstrumentationRegistry.getInstrumentation().targetContext private lateinit var service: DesktopControllerService @Before fun setUp() { hiltRule.inject() val mockDisplay = context.getSystemService( DisplayManager::class.java ).getDisplay(displayId) val mockWindowContext = context.createDisplayContext(mockDisplay) .createWindowContext(WindowManager.LayoutParams.TYPE_SCREENSHOT, null) val mockWindowManager = mockWindowContext.getSystemService(WindowManager::class.java) mockWindowHelper.stub { on { getWindowContext(any(), any()) } doReturn mockWindowContext on { getWindowManager(any()) } doReturn mockWindowManager } try { val binder = serviceRule.bindService( Intent(context, DesktopControllerService::class.java) ) service = (binder as DesktopControllerService.LocalBinder).getService() // Must manually initialize the lateinit var, b/c normally init by MainActivity val dummyDeferredBitmap = CompletableDeferred(Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888)) service.displayIdToDeferredScreenshotMap = mapOf(Pair(displayId, dummyDeferredBitmap)) } catch (e: TimeoutException) { throw RuntimeException("Failed to bind to DesktopController service", e) } } @After fun tearDown() { InstrumentationRegistry.getInstrumentation().runOnMainSync { service.removeUiOverlay() } } @Test fun onBind_returnsBinder() { // Implicitly tested in setUp, binder is obtained there. assertThat(service).isNotNull() } @Test fun showUiOverlay_addsViewAndRegistersListener() { val onSelectColor: (Int) -> Unit = mock() val onAbort: () -> Unit = mock() InstrumentationRegistry.getInstrumentation().runOnMainSync { service.showUiOverlay(onSelectColor, onAbort) } composeTestRule.waitForIdle() val viewCaptor = argumentCaptor() verify( mockWindowHelper ).addFocusedTransparentOverlay(any(), viewCaptor.capture()) verify(mockInputHelper).registerInputDeviceListener(eq(mockInputDeviceListener)) verify(mockInputDeviceListener).initialize(any()) val capturedView = viewCaptor.firstValue assertThat(capturedView).isInstanceOf(ComposeView::class.java) assertThat(capturedView.findViewTreeLifecycleOwner()).isSameInstanceAs(service) } @Test fun removeUiOverlay_removesViewAndUnregistersListener() { val onSelectColor: (Int) -> Unit = mock() val onAbort: () -> Unit = mock() InstrumentationRegistry.getInstrumentation().runOnMainSync { service.showUiOverlay(onSelectColor, onAbort) } composeTestRule.waitForIdle() InstrumentationRegistry.getInstrumentation().runOnMainSync { service.removeUiOverlay() } composeTestRule.waitForIdle() verify(mockWindowHelper).removeView(any(), any()) verify(mockInputHelper).unregisterInputDeviceListener(mockInputDeviceListener) } @Test fun wrappedOnAbort_removesOverlayAndCallsCallback() { val onSelectColor: (Int) -> Unit = mock() val onAbort: () -> Unit = mock() InstrumentationRegistry.getInstrumentation().runOnMainSync { service.showUiOverlay(onSelectColor, onAbort) } composeTestRule.waitForIdle() val abortCaptor = argumentCaptor<() -> Unit>() verify(mockInputDeviceListener).initialize(abortCaptor.capture()) InstrumentationRegistry.getInstrumentation().runOnMainSync { abortCaptor.firstValue.invoke() } composeTestRule.waitForIdle() verify(mockWindowHelper).removeView(any(), any()) verify(mockInputHelper).unregisterInputDeviceListener(mockInputDeviceListener) verify(onAbort).invoke() } }