diff options
Diffstat (limited to 'tests')
35 files changed, 1878 insertions, 232 deletions
diff --git a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java index 08430f2f2744..4143f595f9a0 100644 --- a/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java +++ b/tests/BatteryStatsPerfTest/src/com/android/internal/os/BatteryUsageStatsPerfTest.java @@ -159,7 +159,7 @@ public class BatteryUsageStatsPerfTest { private static BatteryUsageStats buildBatteryUsageStats() { final BatteryUsageStats.Builder builder = - new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, 0) + new BatteryUsageStats.Builder(new String[]{"FOO"}, true, false, false, false, 0) .setBatteryCapacity(4000) .setDischargePercentage(20) .setDischargedPowerRange(1000, 2000) diff --git a/tests/FlickerTests/IME/Android.bp b/tests/FlickerTests/IME/Android.bp index ccc3683f0b93..78d93e1cb32a 100644 --- a/tests/FlickerTests/IME/Android.bp +++ b/tests/FlickerTests/IME/Android.bp @@ -34,6 +34,11 @@ filegroup { srcs: ["src/**/Close*"], } +filegroup { + name: "FlickerTestsIme2-src", + srcs: ["src/**/ShowImeOnAppStart*"], +} + android_test { name: "FlickerTestsIme", defaults: ["FlickerTestsDefault"], @@ -77,9 +82,23 @@ android_test { defaults: ["FlickerTestsDefault"], manifest: "AndroidManifest.xml", test_config_template: "AndroidTestTemplate.xml", + srcs: [":FlickerTestsIme2-src"], + static_libs: [ + "FlickerTestsBase", + "FlickerTestsImeCommon", + ], + data: ["trace_config/*"], +} + +android_test { + name: "FlickerTestsIme3", + defaults: ["FlickerTestsDefault"], + manifest: "AndroidManifest.xml", + test_config_template: "AndroidTestTemplate.xml", srcs: ["src/**/*"], exclude_srcs: [ ":FlickerTestsIme1-src", + ":FlickerTestsIme2-src", ":FlickerTestsImeCommon-src", ], static_libs: [ diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 9a5e88becf1e..8811e00f9661 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -17,6 +17,7 @@ package com.android.server.wm.flicker.helpers import android.graphics.Rect +import android.platform.uiautomator_helpers.DeviceHelpers import android.tools.device.apphelpers.IStandardAppHelper import android.tools.helpers.SYSTEMUI_PACKAGE import android.tools.traces.parsers.WindowManagerStateHelper @@ -26,6 +27,7 @@ import androidx.test.uiautomator.BySelector import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiObject2 import androidx.test.uiautomator.Until +import java.time.Duration /** * Wrapper class around App helper classes. This class adds functionality to the apps that the @@ -41,16 +43,6 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : RIGHT_BOTTOM } - private val TIMEOUT_MS = 3_000L - private val CAPTION = "desktop_mode_caption" - private val CAPTION_HANDLE = "caption_handle" - private val MAXIMIZE_BUTTON = "maximize_window" - private val MAXIMIZE_BUTTON_VIEW = "maximize_button_view" - private val CLOSE_BUTTON = "close_window" - - private val caption: BySelector - get() = By.res(SYSTEMUI_PACKAGE, CAPTION) - /** Wait for an app moved to desktop to finish its transition. */ private fun waitForAppToMoveToDesktop(wmHelper: WindowManagerStateHelper) { wmHelper @@ -119,11 +111,11 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ): UiObject2? { if ( wmHelper.getWindow(innerHelper)?.windowingMode != - WindowingMode.WINDOWING_MODE_FREEFORM.value + WindowingMode.WINDOWING_MODE_FREEFORM.value ) error("expected a freeform window with caption but window is not in freeform mode") val captions = - device.wait(Until.findObjects(caption), TIMEOUT_MS) + device.wait(Until.findObjects(caption), TIMEOUT.toMillis()) ?: error("Unable to find view $caption\n") return captions.find { @@ -147,7 +139,17 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : val endX = startX + horizontalChange // drag the specified corner of the window to the end coordinate. - device.drag(startX, startY, endX, endY, 100) + dragWindow(startX, startY, endX, endY, wmHelper, device) + } + + /** Drag a window from a source coordinate to a destination coordinate. */ + fun dragWindow( + startX: Int, startY: Int, + endX: Int, endY: Int, + wmHelper: WindowManagerStateHelper, + device: UiDevice + ) { + device.drag(startX, startY, endX, endY, /* steps= */ 100) wmHelper .StateSyncBuilder() .withAppTransitionIdle() @@ -165,4 +167,75 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : Corners.RIGHT_BOTTOM -> Pair(windowRect.right, windowRect.bottom) } } + + /** Exit desktop mode by dragging the app handle to the top drag zone. */ + fun exitDesktopWithDragToTopDragZone( + wmHelper: WindowManagerStateHelper, + device: UiDevice, + ) { + dragAppWindowToTopDragZone(wmHelper, device) + waitForTransitionToFullscreen(wmHelper) + } + + private fun dragAppWindowToTopDragZone(wmHelper: WindowManagerStateHelper, device: UiDevice) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val displayRect = + wmHelper.currentState.wmState.getDefaultDisplay()?.displayRect + ?: throw IllegalStateException("Default display is null") + + val startX = windowRect.centerX() + val endX = displayRect.centerX() + val startY = windowRect.top + val endY = 0 // top of the screen + + // drag the app window to top drag zone + device.drag(startX, startY, endX, endY, 100) + } + + fun enterDesktopModeFromAppHandleMenu( + wmHelper: WindowManagerStateHelper, + device: UiDevice) { + val windowRect = wmHelper.getWindowRegion(innerHelper).bounds + val startX = windowRect.centerX() + // Click a little under the top to prevent opening the notification shade. + val startY = 10 + + // Click on the app handle coordinates. + device.click(startX, startY) + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + + val pill = getAppHandlePillForWindow() + val desktopModeButton = + pill + ?.children + ?.find { it.resourceName.endsWith(DESKTOP_MODE_BUTTON) } + + desktopModeButton?.click() + wmHelper.StateSyncBuilder().withAppTransitionIdle().waitForAndVerify() + } + + private fun getAppHandlePillForWindow(): UiObject2? { + val pillContainer: BySelector = By.res(SYSTEMUI_PACKAGE, PILL_CONTAINER) + return DeviceHelpers.waitForObj(pillContainer, TIMEOUT) + } + + /** Wait for transition to full screen to finish. */ + private fun waitForTransitionToFullscreen(wmHelper: WindowManagerStateHelper) { + wmHelper + .StateSyncBuilder() + .withFullScreenApp(innerHelper) + .withAppTransitionIdle() + .waitForAndVerify() + } + + private companion object { + val TIMEOUT = Duration.ofSeconds(3) + val CAPTION = "desktop_mode_caption" + val MAXIMIZE_BUTTON_VIEW = "maximize_button_view" + val CLOSE_BUTTON = "close_window" + val PILL_CONTAINER = "windowing_pill" + val DESKTOP_MODE_BUTTON = "desktop_button" + val caption: BySelector + get() = By.res(SYSTEMUI_PACKAGE, CAPTION) + } } diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml index 36cbf1a8fe84..365a0ea017f6 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_pip.xml @@ -27,15 +27,6 @@ where things are arranged differently and to circle back up to the top once we reach the bottom. --> - <!-- View used for testing sourceRectHint. --> - <View - android:id="@+id/source_rect" - android:layout_width="320dp" - android:layout_height="180dp" - android:visibility="gone" - android:background="@android:color/holo_green_light" - /> - <Button android:id="@+id/enter_pip" android:layout_width="wrap_content" @@ -122,12 +113,11 @@ android:onClick="onRatioSelected"/> </RadioGroup> - <Button + <CheckBox android:id="@+id/set_source_rect_hint" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Set SourceRectHint" - android:onClick="setSourceRectHint"/> + android:text="Set SourceRectHint"/> <TextView android:layout_width="wrap_content" diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java index 27eb5a06451a..13d7f7f0d521 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/PipActivity.java @@ -43,10 +43,10 @@ import android.media.MediaMetadata; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.os.Bundle; +import android.util.DisplayMetrics; import android.util.Log; import android.util.Rational; import android.view.View; -import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.widget.CheckBox; @@ -70,7 +70,7 @@ public class PipActivity extends Activity { */ private static final String TITLE_STATE_PAUSED = "TestApp media is paused"; - private static final Rational RATIO_DEFAULT = null; + private static final Rational RATIO_DEFAULT = new Rational(16, 9); private static final Rational RATIO_SQUARE = new Rational(1, 1); private static final Rational RATIO_WIDE = new Rational(2, 1); private static final Rational RATIO_TALL = new Rational(1, 2); @@ -88,8 +88,7 @@ public class PipActivity extends Activity { "com.android.wm.shell.flicker.testapp.ASPECT_RATIO"; private final PictureInPictureParams.Builder mPipParamsBuilder = - new PictureInPictureParams.Builder() - .setAspectRatio(RATIO_DEFAULT); + new PictureInPictureParams.Builder(); private MediaSession mMediaSession; private final PlaybackState.Builder mPlaybackStateBuilder = new PlaybackState.Builder() .setActions(ACTION_PLAY | ACTION_PAUSE | ACTION_STOP) @@ -139,6 +138,9 @@ public class PipActivity extends Activity { } }; + private Rational mAspectRatio = RATIO_DEFAULT; + private boolean mEnableSourceRectHint; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -156,6 +158,14 @@ public class PipActivity extends Activity { findViewById(R.id.media_session_stop) .setOnClickListener(v -> updateMediaSessionState(STATE_STOPPED)); + final CheckBox setSourceRectHintCheckBox = findViewById(R.id.set_source_rect_hint); + setSourceRectHintCheckBox.setOnCheckedChangeListener((v, isChecked) -> { + if (mEnableSourceRectHint != isChecked) { + mEnableSourceRectHint = isChecked; + updateSourceRectHint(); + } + }); + mMediaSession = new MediaSession(this, "WMShell_TestApp"); mMediaSession.setPlaybackState(mPlaybackStateBuilder.build()); mMediaSession.setCallback(new MediaSession.Callback() { @@ -250,47 +260,64 @@ public class PipActivity extends Activity { } } + private void updateSourceRectHint() { + if (!mEnableSourceRectHint) return; + // Similar to PipUtils#getEnterPipWithOverlaySrcRectHint, crop the display bounds + // as source rect hint based on the current aspect ratio. + final DisplayMetrics displayMetrics = getResources().getDisplayMetrics(); + final Rect displayBounds = new Rect(0, 0, + displayMetrics.widthPixels, displayMetrics.heightPixels); + final Rect sourceRectHint = getEnterPipWithOverlaySrcRectHint( + displayBounds, mAspectRatio.floatValue()); + mPipParamsBuilder + .setAspectRatio(mAspectRatio) + .setSourceRectHint(sourceRectHint); + setPictureInPictureParams(mPipParamsBuilder.build()); + } + /** - * Adds a temporary view used for testing sourceRectHint. - * + * Crop a Rect matches the aspect ratio and pivots at the center point. + * This is a counterpart of {@link PipUtils#getEnterPipWithOverlaySrcRectHint} */ - public void setSourceRectHint(View v) { - View rectView = findViewById(R.id.source_rect); - if (rectView != null) { - rectView.setVisibility(View.VISIBLE); - rectView.getViewTreeObserver().addOnGlobalLayoutListener( - new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - Rect boundingRect = new Rect(); - rectView.getGlobalVisibleRect(boundingRect); - mPipParamsBuilder.setSourceRectHint(boundingRect); - setPictureInPictureParams(mPipParamsBuilder.build()); - rectView.getViewTreeObserver().removeOnGlobalLayoutListener(this); - } - }); - rectView.invalidate(); // changing the visibility, invalidating to redraw the view + private Rect getEnterPipWithOverlaySrcRectHint(Rect appBounds, float aspectRatio) { + final float appBoundsAspectRatio = appBounds.width() / (float) appBounds.height(); + final int width, height; + int left = appBounds.left; + int top = appBounds.top; + if (appBoundsAspectRatio < aspectRatio) { + width = appBounds.width(); + height = (int) (width / aspectRatio); + top = appBounds.top + (appBounds.height() - height) / 2; + } else { + height = appBounds.height(); + width = (int) (height * aspectRatio); + left = appBounds.left + (appBounds.width() - width) / 2; } + return new Rect(left, top, left + width, top + height); } public void onRatioSelected(View v) { switch (v.getId()) { case R.id.ratio_default: - mPipParamsBuilder.setAspectRatio(RATIO_DEFAULT); + mAspectRatio = RATIO_DEFAULT; break; case R.id.ratio_square: - mPipParamsBuilder.setAspectRatio(RATIO_SQUARE); + mAspectRatio = RATIO_SQUARE; break; case R.id.ratio_wide: - mPipParamsBuilder.setAspectRatio(RATIO_WIDE); + mAspectRatio = RATIO_WIDE; break; case R.id.ratio_tall: - mPipParamsBuilder.setAspectRatio(RATIO_TALL); + mAspectRatio = RATIO_TALL; break; } + setPictureInPictureParams(mPipParamsBuilder.setAspectRatio(mAspectRatio).build()); + if (mEnableSourceRectHint) { + updateSourceRectHint(); + } } private void updateMediaSessionState(int newState) { diff --git a/tests/Input/Android.bp b/tests/Input/Android.bp index f367c38b06e9..06c2651b604d 100644 --- a/tests/Input/Android.bp +++ b/tests/Input/Android.bp @@ -48,6 +48,7 @@ android_test { "testables", "testng", "truth", + "ui-trace-collector", ], libs: [ "android.test.mock", diff --git a/tests/Input/AndroidTest.xml b/tests/Input/AndroidTest.xml index 8db37058af2b..bc9322fbd3dc 100644 --- a/tests/Input/AndroidTest.xml +++ b/tests/Input/AndroidTest.xml @@ -22,6 +22,10 @@ <option name="shell-timeout" value="660s" /> <option name="test-timeout" value="600s" /> <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="device-listeners" value="android.tools.collectors.DefaultUITraceListener"/> + <!-- DefaultUITraceListener args --> + <option name="instrumentation-arg" key="skip_test_success_metrics" value="true"/> + <option name="instrumentation-arg" key="per_class" value="true"/> </test> <object class="com.android.tradefed.testtype.suite.module.TestFailureModuleController" type="module_controller"> @@ -31,7 +35,9 @@ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> <option name="pull-pattern-keys" value="input_.*" /> <!-- Pull files created by tests, like the output of screenshot tests --> - <option name="directory-keys" value="/storage/emulated/0/InputTests" /> + <option name="directory-keys" value="/sdcard/Download/InputTests" /> + <!-- Pull perfetto traces from DefaultUITraceListener --> + <option name="pull-pattern-keys" value="perfetto_file_path*" /> <option name="collect-on-run-ended-only" value="false" /> </metrics_collector> </configuration> diff --git a/tests/Input/assets/testPointerStrokeStyle.png b/tests/Input/assets/testPointerStrokeStyle.png Binary files differnew file mode 100644 index 000000000000..4ddde70b2f0a --- /dev/null +++ b/tests/Input/assets/testPointerStrokeStyle.png diff --git a/tests/Input/res/drawable/test_key_drawable.xml b/tests/Input/res/drawable/test_key_drawable.xml new file mode 100644 index 000000000000..2addf8fcf0bd --- /dev/null +++ b/tests/Input/res/drawable/test_key_drawable.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="4dp" /> + <solid android:color="#ffffffff"/> +</shape>
\ No newline at end of file diff --git a/tests/Input/res/drawable/test_modifier_drawable.xml b/tests/Input/res/drawable/test_modifier_drawable.xml new file mode 100644 index 000000000000..2addf8fcf0bd --- /dev/null +++ b/tests/Input/res/drawable/test_modifier_drawable.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 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. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="4dp" /> + <solid android:color="#ffffffff"/> +</shape>
\ No newline at end of file diff --git a/tests/Input/res/xml/keyboard_glyph_maps.xml b/tests/Input/res/xml/keyboard_glyph_maps.xml new file mode 100644 index 000000000000..d0616ff5ccaa --- /dev/null +++ b/tests/Input/res/xml/keyboard_glyph_maps.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 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. + --> +<keyboard-glyph-maps xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <keyboard-glyph-map + androidprv:glyphMap="@xml/test_glyph_map" + androidprv:vendorId="0x1234" + androidprv:productId="0x3456" /> +</keyboard-glyph-maps>
\ No newline at end of file diff --git a/tests/Input/res/xml/test_glyph_map.xml b/tests/Input/res/xml/test_glyph_map.xml new file mode 100644 index 000000000000..7a7c1accb7fd --- /dev/null +++ b/tests/Input/res/xml/test_glyph_map.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2024 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. + --> +<keyboard-glyph-map xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <key-glyph + androidprv:keycode="KEYCODE_BACK" + androidprv:glyphDrawable="@drawable/test_key_drawable" /> + <modifier-glyph + androidprv:modifier="META" + androidprv:glyphDrawable="@drawable/test_modifier_drawable" /> + <function-row-key androidprv:keycode="KEYCODE_EMOJI_PICKER" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_1" + androidprv:modifierState="FUNCTION" + androidprv:outKeycode="KEYCODE_BACK" /> + <hardware-defined-shortcut + androidprv:keycode="KEYCODE_2" + androidprv:modifierState="FUNCTION|META" + androidprv:outKeycode="KEYCODE_HOME" /> +</keyboard-glyph-map>
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt new file mode 100644 index 000000000000..c073c7aae678 --- /dev/null +++ b/tests/Input/src/com/android/server/input/KeyboardGlyphManagerTests.kt @@ -0,0 +1,197 @@ +/* + * Copyright 2024 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.server.input + +import android.content.Context +import android.content.ContextWrapper +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.content.pm.ServiceInfo +import android.hardware.input.IInputManager +import android.hardware.input.InputManager +import android.hardware.input.InputManagerGlobal +import android.hardware.input.KeyGlyphMap.KeyCombination +import android.os.Bundle +import android.os.test.TestLooper +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.view.InputDevice +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.android.hardware.input.Flags +import com.android.test.input.R +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit + +/** + * Tests for custom keyboard glyph map configuration. + * + * Build/Install/Run: + * atest InputTests:KeyboardGlyphManagerTests + */ +@Presubmit +class KeyboardGlyphManagerTests { + + companion object { + const val DEVICE_ID = 1 + const val VENDOR_ID = 0x1234 + const val PRODUCT_ID = 0x3456 + const val PACKAGE_NAME = "KeyboardLayoutManagerTests" + const val RECEIVER_NAME = "DummyReceiver" + } + + @JvmField + @Rule(order = 0) + val setFlagsRule = SetFlagsRule() + + @JvmField + @Rule(order = 1) + val mockitoRule = MockitoJUnit.rule()!! + + @Mock + private lateinit var packageManager: PackageManager + + @Mock + private lateinit var iInputManager: IInputManager + + private lateinit var keyboardGlyphManager: KeyboardGlyphManager + private lateinit var context: Context + private lateinit var testLooper: TestLooper + private lateinit var inputManagerGlobalSession: InputManagerGlobal.TestSession + private lateinit var keyboardDevice: InputDevice + + @Before + fun setup() { + context = Mockito.spy(ContextWrapper(ApplicationProvider.getApplicationContext())) + inputManagerGlobalSession = InputManagerGlobal.createTestSession(iInputManager) + testLooper = TestLooper() + keyboardGlyphManager = KeyboardGlyphManager(context, testLooper.looper) + + setupInputDevices() + setupBroadcastReceiver() + keyboardGlyphManager.systemRunning() + testLooper.dispatchAll() + } + + @After + fun tearDown() { + if (this::inputManagerGlobalSession.isInitialized) { + inputManagerGlobalSession.close() + } + } + + private fun setupInputDevices() { + val inputManager = InputManager(context) + Mockito.`when`(context.getSystemService(Mockito.eq(Context.INPUT_SERVICE))) + .thenReturn(inputManager) + + keyboardDevice = createKeyboard(DEVICE_ID, VENDOR_ID, PRODUCT_ID, 0, "", "") + Mockito.`when`(iInputManager.inputDeviceIds).thenReturn(intArrayOf(DEVICE_ID)) + Mockito.`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardDevice) + } + + private fun setupBroadcastReceiver() { + Mockito.`when`(context.packageManager).thenReturn(packageManager) + + val info = createMockReceiver() + Mockito.`when`(packageManager.queryBroadcastReceiversAsUser(Mockito.any(), Mockito.anyInt(), + Mockito.anyInt())).thenReturn(listOf(info)) + Mockito.`when`(packageManager.getReceiverInfo(Mockito.any(), Mockito.anyInt())) + .thenReturn(info.activityInfo) + + val resources = context.resources + Mockito.`when`( + packageManager.getResourcesForApplication( + Mockito.any( + ApplicationInfo::class.java + ) + ) + ).thenReturn(resources) + } + + private fun createMockReceiver(): ResolveInfo { + val info = ResolveInfo() + info.activityInfo = ActivityInfo() + info.activityInfo.packageName = PACKAGE_NAME + info.activityInfo.name = RECEIVER_NAME + info.activityInfo.applicationInfo = ApplicationInfo() + info.activityInfo.metaData = Bundle() + info.activityInfo.metaData.putInt( + InputManager.META_DATA_KEYBOARD_GLYPH_MAPS, + R.xml.keyboard_glyph_maps + ) + info.serviceInfo = ServiceInfo() + info.serviceInfo.packageName = PACKAGE_NAME + info.serviceInfo.name = RECEIVER_NAME + return info + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP) + fun testGlyphMapsLoaded() { + assertNotNull( + "Glyph map for test keyboard(deviceId=$DEVICE_ID) must exist", + keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID) + ) + assertNull( + "Glyph map for non-existing keyboard must be null", + keyboardGlyphManager.getKeyGlyphMap(-2) + ) + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_GLYPH_MAP) + fun testGlyphMapCorrectlyLoaded() { + val glyphMap = keyboardGlyphManager.getKeyGlyphMap(DEVICE_ID) + // Test glyph map used in this test: {@see test_glyph_map.xml} + assertNotNull(glyphMap!!.getDrawableForKeycode(context, KeyEvent.KEYCODE_BACK)) + + assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_LEFT)) + assertNotNull(glyphMap.getDrawableForModifier(context, KeyEvent.KEYCODE_META_RIGHT)) + + val functionRowKeys = glyphMap.functionRowKeys + assertEquals(1, functionRowKeys.size) + assertEquals(KeyEvent.KEYCODE_EMOJI_PICKER, functionRowKeys[0]) + + val hardwareShortcuts = glyphMap.hardwareShortcuts + assertEquals(2, hardwareShortcuts.size) + assertEquals( + KeyEvent.KEYCODE_BACK, + hardwareShortcuts[KeyCombination(KeyEvent.META_FUNCTION_ON, KeyEvent.KEYCODE_1)] + ) + assertEquals( + KeyEvent.KEYCODE_HOME, + hardwareShortcuts[ + KeyCombination( + KeyEvent.META_FUNCTION_ON or KeyEvent.META_META_ON, + KeyEvent.KEYCODE_2 + ) + ] + ) + } +} diff --git a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt index 93f97cb4a7ee..301c0e6a159f 100644 --- a/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyboardLayoutManagerTests.kt @@ -58,7 +58,7 @@ import java.io.FileOutputStream import java.io.IOException import java.io.InputStream -private fun createKeyboard( +fun createKeyboard( deviceId: Int, vendorId: Int, productId: Int, diff --git a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt index d196b85a7466..abfe549f3d22 100644 --- a/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt +++ b/tests/Input/src/com/android/test/input/PointerIconLoadingTest.kt @@ -19,7 +19,6 @@ package com.android.test.input import android.content.Context import android.content.res.Configuration import android.content.res.Resources -import android.os.Environment import android.view.ContextThemeWrapper import android.view.PointerIcon import android.view.flags.Flags.enableVectorCursorA11ySettings @@ -88,6 +87,35 @@ class PointerIconLoadingTest { theme.applyStyle( PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_GREEN), /* force= */ true) + theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), /* force= */ true) + + val pointerIcon = + PointerIcon.getLoadedSystemIcon( + ContextThemeWrapper(context, theme), + PointerIcon.TYPE_ARROW, + /* useLargeIcons= */ false, + /* pointerScale= */ 1f) + + pointerIcon.getBitmap().assertAgainstGolden( + screenshotRule, + testName.methodName, + exactScreenshotMatcher + ) + } + + @Test + fun testPointerStrokeStyle() { + assumeTrue(enableVectorCursors()) + assumeTrue(enableVectorCursorA11ySettings()) + + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), + /* force= */ true) + theme.applyStyle(PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_BLACK), /* force= */ true) val pointerIcon = PointerIcon.getLoadedSystemIcon( @@ -108,11 +136,20 @@ class PointerIconLoadingTest { assumeTrue(enableVectorCursors()) assumeTrue(enableVectorCursorA11ySettings()) + val theme: Resources.Theme = context.getResources().newTheme() + theme.setTo(context.getTheme()) + theme.applyStyle( + PointerIcon.vectorFillStyleToResource(PointerIcon.POINTER_ICON_VECTOR_STYLE_FILL_BLACK), + /* force= */ true) + theme.applyStyle( + PointerIcon.vectorStrokeStyleToResource( + PointerIcon.POINTER_ICON_VECTOR_STYLE_STROKE_WHITE), + /* force= */ true) val pointerScale = 2f val pointerIcon = PointerIcon.getLoadedSystemIcon( - context, + ContextThemeWrapper(context, theme), PointerIcon.TYPE_ARROW, /* useLargeIcons= */ false, pointerScale) @@ -129,8 +166,7 @@ class PointerIconLoadingTest { const val SCREEN_WIDTH_DP = 480 const val SCREEN_HEIGHT_DP = 800 const val ASSETS_PATH = "tests/input/assets" - val TEST_OUTPUT_PATH = Environment.getExternalStorageDirectory().absolutePath + - "/InputTests/" + - PointerIconLoadingTest::class.java.simpleName + val TEST_OUTPUT_PATH = + "/sdcard/Download/InputTests/" + PointerIconLoadingTest::class.java.simpleName } } diff --git a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt index 63782f1bf955..1842f0a64a83 100644 --- a/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt +++ b/tests/Input/src/com/android/test/input/UnresponsiveGestureMonitorActivity.kt @@ -14,6 +14,9 @@ * limitations under the License. */ +// InputMonitor is deprecated, but we still need to test it. +@file:Suppress("DEPRECATION") + package com.android.test.input import android.app.Activity @@ -43,6 +46,7 @@ class UnresponsiveGestureMonitorActivity : Activity() { } private lateinit var mInputEventReceiver: InputEventReceiver private lateinit var mInputMonitor: InputMonitor + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val inputManager = checkNotNull(getSystemService(InputManager::class.java)) diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java index e60d8efdbfa4..a2c3572eca9b 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/AutoShowTest.java @@ -37,14 +37,14 @@ import android.content.res.Configuration; import android.os.SystemClock; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.By; -import android.support.test.uiautomator.UiDevice; -import android.support.test.uiautomator.UiObject2; -import android.support.test.uiautomator.Until; import android.view.WindowManager; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.By; +import androidx.test.uiautomator.UiDevice; +import androidx.test.uiautomator.UiObject2; +import androidx.test.uiautomator.Until; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java index 2ac25f2696d3..b994bfb00007 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/DefaultImeVisibilityTest.java @@ -33,10 +33,10 @@ import static com.google.common.truth.Truth.assertWithMessage; import android.content.Intent; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.UiDevice; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java index 5368025ff898..2128cbf90542 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeOpenCloseStressTest.java @@ -48,12 +48,12 @@ import android.os.Build; import android.os.SystemClock; import android.platform.test.annotations.RootPermissionTest; import android.platform.test.rule.UnlockScreenRule; -import android.support.test.uiautomator.UiDevice; import android.util.Log; import android.view.WindowManager; import android.widget.EditText; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.Rule; import org.junit.Test; diff --git a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java index c7463218b646..1249a4564e8e 100644 --- a/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java +++ b/tests/InputMethodStressTest/src/com/android/inputmethod/stresstest/ImeStressTestRule.java @@ -18,10 +18,10 @@ package com.android.inputmethod.stresstest; import android.app.Instrumentation; import android.os.RemoteException; -import android.support.test.uiautomator.UiDevice; import androidx.annotation.NonNull; import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.uiautomator.UiDevice; import org.junit.rules.TestWatcher; import org.junit.runner.Description; diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java index 5a27593c7a36..5a48327e7576 100644 --- a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogImplTest.java @@ -59,7 +59,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.util.LinkedList; -import java.util.TreeMap; /** * Test class for {@link ProtoLogImpl}. @@ -90,7 +89,7 @@ public class LegacyProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); mProtoLog = new LegacyProtoLogImpl(mFile, mViewerConfigFilename, - 1024 * 1024, mReader, 1024, new TreeMap<>(), () -> {}); + 1024 * 1024, mReader, 1024, () -> {}); } @After @@ -142,7 +141,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -159,7 +158,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -176,7 +175,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -192,7 +191,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -208,7 +207,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); @@ -277,7 +276,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b1110101001010100, null, + 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -302,7 +301,7 @@ public class LegacyProtoLogImplTest { long before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b01100100, null, + 0b01100100, new Object[]{"test", 1, 0.1, true}); long after = SystemClock.elapsedRealtimeNanos(); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); @@ -326,7 +325,7 @@ public class LegacyProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); mProtoLog.startProtoLog(mock(PrintWriter.class)); mProtoLog.log(LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, - 0b11, null, new Object[]{true}); + 0b11, new Object[]{true}); mProtoLog.stopProtoLog(mock(PrintWriter.class), true); try (InputStream is = new FileInputStream(mFile)) { ProtoInputStream ip = new ProtoInputStream(is); diff --git a/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java new file mode 100644 index 000000000000..253965337824 --- /dev/null +++ b/tests/Internal/src/com/android/internal/protolog/LegacyProtoLogViewerConfigReaderTest.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2019 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.internal.protolog; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import android.platform.test.annotations.Presubmit; + +import androidx.test.filters.SmallTest; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.util.zip.GZIPOutputStream; + +@SmallTest +@Presubmit +@RunWith(JUnit4.class) +public class LegacyProtoLogViewerConfigReaderTest { + private static final String TEST_VIEWER_CONFIG = "{\n" + + " \"version\": \"1.0.0\",\n" + + " \"messages\": {\n" + + " \"70933285\": {\n" + + " \"message\": \"Test completed successfully: %b\",\n" + + " \"level\": \"ERROR\",\n" + + " \"group\": \"GENERIC_WM\"\n" + + " },\n" + + " \"1792430067\": {\n" + + " \"message\": \"Attempted to add window to a display that does not exist: %d." + + " Aborting.\",\n" + + " \"level\": \"WARN\",\n" + + " \"group\": \"GENERIC_WM\"\n" + + " },\n" + + " \"1352021864\": {\n" + + " \"message\": \"Test 2\",\n" + + " \"level\": \"WARN\",\n" + + " \"group\": \"GENERIC_WM\"\n" + + " },\n" + + " \"409412266\": {\n" + + " \"message\": \"Window %s is already added\",\n" + + " \"level\": \"WARN\",\n" + + " \"group\": \"GENERIC_WM\"\n" + + " }\n" + + " },\n" + + " \"groups\": {\n" + + " \"GENERIC_WM\": {\n" + + " \"tag\": \"WindowManager\"\n" + + " }\n" + + " }\n" + + "}\n"; + + + private LegacyProtoLogViewerConfigReader + mConfig = new LegacyProtoLogViewerConfigReader(); + private File mTestViewerConfig; + + @Before + public void setUp() throws IOException { + mTestViewerConfig = File.createTempFile("testConfig", ".json.gz"); + OutputStreamWriter writer = new OutputStreamWriter( + new GZIPOutputStream(new FileOutputStream(mTestViewerConfig))); + writer.write(TEST_VIEWER_CONFIG); + writer.close(); + } + + @After + public void tearDown() { + //noinspection ResultOfMethodCallIgnored + mTestViewerConfig.delete(); + } + + @Test + public void getViewerString_notLoaded() { + assertNull(mConfig.getViewerString(1)); + } + + @Test + public void loadViewerConfig() { + mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath()); + assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285)); + assertEquals("Test 2", mConfig.getViewerString(1352021864)); + assertEquals("Window %s is already added", mConfig.getViewerString(409412266)); + assertNull(mConfig.getViewerString(1)); + } + + @Test + public void loadViewerConfig_invalidFile() { + mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist"); + // No exception is thrown. + assertNull(mConfig.getViewerString(1)); + } +} diff --git a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java index 1d7b6b348e10..fad94d45c85d 100644 --- a/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/PerfettoProtoLogImplTest.java @@ -60,6 +60,9 @@ import org.junit.runners.JUnit4; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import perfetto.protos.Protolog; +import perfetto.protos.ProtologCommon; + import java.io.File; import java.io.IOException; import java.util.List; @@ -67,9 +70,6 @@ import java.util.Random; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicInteger; -import perfetto.protos.Protolog; -import perfetto.protos.ProtologCommon; - /** * Test class for {@link ProtoLogImpl}. */ @@ -111,6 +111,9 @@ public class PerfettoProtoLogImplTest { //noinspection ResultOfMethodCallIgnored mFile.delete(); + TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); + TestProtoLogGroup.TEST_GROUP.setLogToProto(false); + mViewerConfigBuilder = Protolog.ProtoLogViewerConfig.newBuilder() .addGroups( Protolog.ProtoLogViewerConfig.Group.newBuilder() @@ -157,8 +160,9 @@ public class PerfettoProtoLogImplTest { mCacheUpdater = () -> {}; mReader = Mockito.spy(new ProtoLogViewerConfigReader(viewerConfigInputStreamProvider)); mProtoLog = new PerfettoProtoLogImpl( - viewerConfigInputStreamProvider, mReader, new TreeMap<>(), + viewerConfigInputStreamProvider, mReader, () -> mCacheUpdater.run()); + mProtoLog.registerGroups(TestProtoLogGroup.values()); } @After @@ -210,15 +214,15 @@ public class PerfettoProtoLogImplTest { // Shouldn't be logging anything except WTF unless explicitly requested in the group // override. mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -240,15 +244,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -274,15 +278,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -304,15 +308,15 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.VERBOSE, TestProtoLogGroup.TEST_GROUP, 2, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WARN, TestProtoLogGroup.TEST_GROUP, 3, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.ERROR, TestProtoLogGroup.TEST_GROUP, 4, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); mProtoLog.log(LogLevel.WTF, TestProtoLogGroup.TEST_GROUP, 5, - LogDataType.BOOLEAN, null, new Object[]{true}); + LogDataType.BOOLEAN, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -329,14 +333,14 @@ public class PerfettoProtoLogImplTest { } @Test - public void log_logcatEnabledExternalMessage() { + public void log_logcatEnabled() { when(mReader.getViewerString(anyLong())).thenReturn("test %b %d %% 0x%x %s %f"); PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 30000, "test", 0.000003}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( @@ -353,32 +357,17 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{true, 10000, 0.0001, 0.00002, "test"}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( LogLevel.INFO), - eq("UNKNOWN MESSAGE (1234) true 10000 1.0E-4 2.0E-5 test")); + eq("FORMAT_ERROR \"test %b %d %% %x %s %f\", " + + "args=(true, 10000, 1.0E-4, 2.0E-5, test)")); verify(mReader).getViewerString(eq(1234L)); } @Test - public void log_logcatEnabledInlineMessage() { - when(mReader.getViewerString(anyLong())).thenReturn("test %d"); - PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); - TestProtoLogGroup.TEST_GROUP.setLogToLogcat(true); - TestProtoLogGroup.TEST_GROUP.setLogToProto(false); - - implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", - new Object[]{5}); - - verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("test 5")); - verify(mReader, never()).getViewerString(anyLong()); - } - - @Test public void log_logcatEnabledNoMessage() { when(mReader.getViewerString(anyLong())).thenReturn(null); PerfettoProtoLogImpl implSpy = Mockito.spy(mProtoLog); @@ -386,11 +375,11 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToProto(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, null, + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy).passToLogcat(eq(TestProtoLogGroup.TEST_GROUP.getTag()), eq( - LogLevel.INFO), eq("UNKNOWN MESSAGE (1234) 5")); + LogLevel.INFO), eq("UNKNOWN MESSAGE#1234 (5)")); verify(mReader).getViewerString(eq(1234L)); } @@ -401,7 +390,7 @@ public class PerfettoProtoLogImplTest { TestProtoLogGroup.TEST_GROUP.setLogToLogcat(false); implSpy.log( - LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d", + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, 1234, 4321, new Object[]{5}); verify(implSpy, never()).passToLogcat(any(), any(), any()); @@ -425,7 +414,7 @@ public class PerfettoProtoLogImplTest { before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b1110101001010100, null, + 0b1110101001010100, new Object[]{"test", 1, 2, 3, 0.4, 0.5, 0.6, true}); after = SystemClock.elapsedRealtimeNanos(); } finally { @@ -444,6 +433,38 @@ public class PerfettoProtoLogImplTest { .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, 5.000000e-01, 0.6, true"); } + @Test + public void log_noProcessing() throws IOException { + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog().build(); + long before; + long after; + try { + traceMonitor.start(); + assertTrue(mProtoLog.isProtoEnabled()); + + before = SystemClock.elapsedRealtimeNanos(); + mProtoLog.log( + LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, + "My test message :: %s, %d, %o, %x, %f, %b", + "test", 1, 2, 3, 0.4, true); + after = SystemClock.elapsedRealtimeNanos(); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) + .isAtLeast(before); + Truth.assertThat(protolog.messages.getFirst().getTimestamp().getElapsedNanos()) + .isAtMost(after); + Truth.assertThat(protolog.messages.getFirst().getMessage()) + .isEqualTo("My test message :: test, 2, 4, 6, 0.400000, true"); + } + private long addMessageToConfig(ProtologCommon.ProtoLogLevel logLevel, String message) { final long messageId = new Random().nextLong(); mViewerConfigBuilder.addMessages(Protolog.ProtoLogViewerConfig.MessageData.newBuilder() @@ -470,7 +491,7 @@ public class PerfettoProtoLogImplTest { before = SystemClock.elapsedRealtimeNanos(); mProtoLog.log( LogLevel.INFO, TestProtoLogGroup.TEST_GROUP, messageHash, - 0b01100100, null, + 0b01100100, new Object[]{"test", 1, 0.1, true}); after = SystemClock.elapsedRealtimeNanos(); } finally { @@ -488,7 +509,7 @@ public class PerfettoProtoLogImplTest { try { traceMonitor.start(); mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, 1, - 0b11, null, new Object[]{true}); + 0b11, new Object[]{true}); } finally { traceMonitor.stop(mWriter); } @@ -512,7 +533,7 @@ public class PerfettoProtoLogImplTest { ProtoLogImpl.setSingleInstance(mProtoLog); ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1, - 0b11, null, true); + 0b11, true); } finally { traceMonitor.stop(mWriter); } @@ -586,7 +607,7 @@ public class PerfettoProtoLogImplTest { .isFalse(); Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); - Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isTrue(); + Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)).isFalse(); PerfettoTraceMonitor traceMonitor1 = PerfettoTraceMonitor.newBuilder().enableProtoLog(true, @@ -664,7 +685,53 @@ public class PerfettoProtoLogImplTest { Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.ERROR)) .isFalse(); Truth.assertThat(mProtoLog.isEnabled(TestProtoLogGroup.TEST_GROUP, LogLevel.WTF)) - .isTrue(); + .isFalse(); + } + + @Test + public void supportsNullString() throws IOException { + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true) + .build(); + + try { + traceMonitor.start(); + + mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "My test null string: %s", (Object) null); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("My test null string: null"); + } + + @Test + public void supportNullParams() throws IOException { + PerfettoTraceMonitor traceMonitor = + PerfettoTraceMonitor.newBuilder().enableProtoLog(true) + .build(); + + try { + traceMonitor.start(); + + mProtoLog.log(LogLevel.DEBUG, TestProtoLogGroup.TEST_GROUP, + "My null args: %d, %f, %b", null, null, null); + } finally { + traceMonitor.stop(mWriter); + } + + final ResultReader reader = new ResultReader(mWriter.write(), mTraceConfig); + final ProtoLogTrace protolog = reader.readProtoLogTrace(); + + Truth.assertThat(protolog.messages).hasSize(1); + Truth.assertThat(protolog.messages.get(0).getMessage()) + .isEqualTo("My null args: 0, 0, false"); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java index 60456f9ea10f..0496240f01e4 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogImplTest.java @@ -58,51 +58,50 @@ public class ProtoLogImplTest { public void d_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.d(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.DEBUG), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void v_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.v(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.VERBOSE), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void i_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.i(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.INFO), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void w_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, - 4321, "test %d"); + ProtoLogImpl.w(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.WARN), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test public void e_logCalled() { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); - ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321, "test %d"); + ProtoLogImpl.e(TestProtoLogGroup.TEST_GROUP, 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.ERROR), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } @Test @@ -110,10 +109,10 @@ public class ProtoLogImplTest { IProtoLog mockedProtoLog = mock(IProtoLog.class); ProtoLogImpl.setSingleInstance(mockedProtoLog); ProtoLogImpl.wtf(TestProtoLogGroup.TEST_GROUP, - 1234, 4321, "test %d"); + 1234, 4321); verify(mockedProtoLog).log(eq(LogLevel.WTF), eq( TestProtoLogGroup.TEST_GROUP), - eq(1234L), eq(4321), eq("test %d"), eq(new Object[]{})); + eq(1234L), eq(4321), eq(new Object[]{})); } private enum TestProtoLogGroup implements IProtoLogGroup { diff --git a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java index dbd85d38b7f2..be0e8bc0fc07 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtoLogViewerConfigReaderTest.java @@ -20,75 +20,77 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import android.platform.test.annotations.Presubmit; +import android.util.proto.ProtoInputStream; -import androidx.test.filters.SmallTest; - -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.util.zip.GZIPOutputStream; +import perfetto.protos.Protolog; +import perfetto.protos.ProtologCommon; -@SmallTest @Presubmit @RunWith(JUnit4.class) public class ProtoLogViewerConfigReaderTest { - private static final String TEST_VIEWER_CONFIG = "{\n" - + " \"version\": \"1.0.0\",\n" - + " \"messages\": {\n" - + " \"70933285\": {\n" - + " \"message\": \"Test completed successfully: %b\",\n" - + " \"level\": \"ERROR\",\n" - + " \"group\": \"GENERIC_WM\"\n" - + " },\n" - + " \"1792430067\": {\n" - + " \"message\": \"Attempted to add window to a display that does not exist: %d." - + " Aborting.\",\n" - + " \"level\": \"WARN\",\n" - + " \"group\": \"GENERIC_WM\"\n" - + " },\n" - + " \"1352021864\": {\n" - + " \"message\": \"Test 2\",\n" - + " \"level\": \"WARN\",\n" - + " \"group\": \"GENERIC_WM\"\n" - + " },\n" - + " \"409412266\": {\n" - + " \"message\": \"Window %s is already added\",\n" - + " \"level\": \"WARN\",\n" - + " \"group\": \"GENERIC_WM\"\n" - + " }\n" - + " },\n" - + " \"groups\": {\n" - + " \"GENERIC_WM\": {\n" - + " \"tag\": \"WindowManager\"\n" - + " }\n" - + " }\n" - + "}\n"; + private static final String TEST_GROUP_NAME = "MY_TEST_GROUP"; + private static final String TEST_GROUP_TAG = "TEST"; + private static final String OTHER_TEST_GROUP_NAME = "MY_OTHER_TEST_GROUP"; + private static final String OTHER_TEST_GROUP_TAG = "OTHER_TEST"; - private LegacyProtoLogViewerConfigReader - mConfig = new LegacyProtoLogViewerConfigReader(); - private File mTestViewerConfig; + private static final byte[] TEST_VIEWER_CONFIG = + perfetto.protos.Protolog.ProtoLogViewerConfig.newBuilder() + .addGroups( + perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(TEST_GROUP_NAME) + .setTag(TEST_GROUP_TAG) + ).addGroups( + perfetto.protos.Protolog.ProtoLogViewerConfig.Group.newBuilder() + .setId(1) + .setName(OTHER_TEST_GROUP_NAME) + .setTag(OTHER_TEST_GROUP_TAG) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(1) + .setMessage("My Test Log Message 1 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_DEBUG) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(2) + .setMessage("My Test Log Message 2 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_VERBOSE) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(3) + .setMessage("My Test Log Message 3 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WARN) + .setGroupId(1) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(4) + .setMessage("My Test Log Message 4 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_ERROR) + .setGroupId(2) + ).addMessages( + perfetto.protos.Protolog.ProtoLogViewerConfig.MessageData.newBuilder() + .setMessageId(5) + .setMessage("My Test Log Message 5 %b") + .setLevel(ProtologCommon.ProtoLogLevel.PROTOLOG_LEVEL_WTF) + .setGroupId(2) + ).build().toByteArray(); - @Before - public void setUp() throws IOException { - mTestViewerConfig = File.createTempFile("testConfig", ".json.gz"); - OutputStreamWriter writer = new OutputStreamWriter( - new GZIPOutputStream(new FileOutputStream(mTestViewerConfig))); - writer.write(TEST_VIEWER_CONFIG); - writer.close(); - } + private final ViewerConfigInputStreamProvider mViewerConfigInputStreamProvider = + () -> new ProtoInputStream(TEST_VIEWER_CONFIG); + + private ProtoLogViewerConfigReader mConfig; - @After - public void tearDown() { - //noinspection ResultOfMethodCallIgnored - mTestViewerConfig.delete(); + @Before + public void before() { + mConfig = new ProtoLogViewerConfigReader(mViewerConfigInputStreamProvider); } @Test @@ -98,17 +100,26 @@ public class ProtoLogViewerConfigReaderTest { @Test public void loadViewerConfig() { - mConfig.loadViewerConfig(msg -> {}, mTestViewerConfig.getAbsolutePath()); - assertEquals("Test completed successfully: %b", mConfig.getViewerString(70933285)); - assertEquals("Test 2", mConfig.getViewerString(1352021864)); - assertEquals("Window %s is already added", mConfig.getViewerString(409412266)); - assertNull(mConfig.getViewerString(1)); + mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME }); + assertEquals("My Test Log Message 1 %b", mConfig.getViewerString(1)); + assertEquals("My Test Log Message 2 %b", mConfig.getViewerString(2)); + assertEquals("My Test Log Message 3 %b", mConfig.getViewerString(3)); + assertNull(mConfig.getViewerString(4)); + assertNull(mConfig.getViewerString(5)); } @Test - public void loadViewerConfig_invalidFile() { - mConfig.loadViewerConfig(msg -> {}, "/tmp/unknown/file/does/not/exist"); - // No exception is thrown. + public void unloadViewerConfig() { + mConfig.loadViewerConfig(new String[] { TEST_GROUP_NAME, OTHER_TEST_GROUP_NAME }); + mConfig.unloadViewerConfig(new String[] { TEST_GROUP_NAME }); assertNull(mConfig.getViewerString(1)); + assertNull(mConfig.getViewerString(2)); + assertNull(mConfig.getViewerString(3)); + assertEquals("My Test Log Message 4 %b", mConfig.getViewerString(4)); + assertEquals("My Test Log Message 5 %b", mConfig.getViewerString(5)); + + mConfig.unloadViewerConfig(new String[] { OTHER_TEST_GROUP_NAME }); + assertNull(mConfig.getViewerString(4)); + assertNull(mConfig.getViewerString(5)); } } diff --git a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java index be9fb1b309f6..9a062e3b2f80 100644 --- a/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java +++ b/tests/Internal/src/com/android/internal/protolog/ProtologDataSourceTest.java @@ -67,7 +67,7 @@ public class ProtologDataSourceTest { @Test public void allEnabledTraceMode() { - final ProtoLogDataSource ds = new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {}); + final ProtoLogDataSource ds = new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {}); final ProtoLogDataSource.TlsState tlsState = createTlsState( DataSourceConfigOuterClass.DataSourceConfig.newBuilder().setProtologConfig( @@ -154,7 +154,7 @@ public class ProtologDataSourceTest { private ProtoLogDataSource.TlsState createTlsState( DataSourceConfigOuterClass.DataSourceConfig config) { final ProtoLogDataSource ds = - Mockito.spy(new ProtoLogDataSource((c) -> {}, () -> {}, (c) -> {})); + Mockito.spy(new ProtoLogDataSource((idx, c) -> {}, () -> {}, (idx, c) -> {})); ProtoInputStream configStream = new ProtoInputStream(config.toByteArray()); final ProtoLogDataSource.Instance dsInstance = Mockito.spy( diff --git a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java index a8b383cd4274..ab406ef4632e 100644 --- a/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java +++ b/tests/PackageWatchdog/src/com/android/server/PackageWatchdogTest.java @@ -23,9 +23,12 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; @@ -33,6 +36,7 @@ import static org.mockito.Mockito.when; import android.Manifest; import android.content.Context; +import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.VersionedPackage; @@ -116,6 +120,7 @@ public class PackageWatchdogTest { private ConnectivityModuleConnector mConnectivityModuleConnector; @Mock private PackageManager mMockPackageManager; + @Mock Intent mMockIntent; // Mock only sysprop apis private PackageWatchdog.BootThreshold mSpyBootThreshold; @Captor @@ -1669,6 +1674,19 @@ public class PackageWatchdogTest { PackageWatchdog.DEFAULT_TRIGGER_FAILURE_DURATION_MS); } + /** + * Tests device config changes are propagated correctly. + */ + @Test + public void testRegisterShutdownBroadcastReceiver() { + PackageWatchdog watchdog = createWatchdog(); + doReturn(mMockIntent).when(mSpyContext) + .registerReceiverForAllUsers(any(), any(), any(), any()); + + watchdog.registerShutdownBroadcastReceiver(); + verify(mSpyContext).registerReceiverForAllUsers(any(), any(), eq(null), eq(null)); + } + private void adoptShellPermissions(String... permissions) { InstrumentationRegistry .getInstrumentation() diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp index 4c531b8f9ee0..a4085e5315a4 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/Android.bp @@ -23,6 +23,7 @@ android_test { resource_dirs: ["res"], libs: ["android.test.runner"], static_libs: [ + "androidx.core_core", "androidx.test.ext.junit", "androidx.test.rules", "compatibility-device-util-axt", diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java index fff1dd1a7cb1..5f9a710c5f78 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/ConcurrentMultiUserTest.java @@ -16,25 +16,40 @@ package com.android.server.inputmethod.multisessiontest; +import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; + import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.getResponderUserId; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.launchActivityAsUserSync; import static com.android.compatibility.common.util.concurrentuser.ConcurrentUserActivityUtils.sendBundleAndWaitForReply; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_EDITTEXT_CENTER; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_IME_SHOWN; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_REQUEST_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_RESULT_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_HIDDEN; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_EDITTEXT_POSITION; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_HIDE_IME; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_IME_STATUS; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_SHOW_IME; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assume.assumeTrue; + +import android.app.UiAutomation; import android.content.ComponentName; +import android.content.Context; import android.os.Bundle; +import android.os.UserHandle; +import android.view.inputmethod.InputMethodInfo; +import android.view.inputmethod.InputMethodManager; import androidx.test.core.app.ActivityScenario; import com.android.bedstead.harrier.BedsteadJUnit4; import com.android.bedstead.harrier.DeviceState; +import com.android.compatibility.common.util.SystemUtil; import org.junit.After; import org.junit.Before; @@ -44,8 +59,10 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import java.io.IOException; +import java.util.List; + @RunWith(BedsteadJUnit4.class) -@Ignore("b/345557347") public final class ConcurrentMultiUserTest { @ClassRule @@ -55,6 +72,10 @@ public final class ConcurrentMultiUserTest { private static final ComponentName TEST_ACTIVITY = new ComponentName( getInstrumentation().getTargetContext().getPackageName(), MainActivity.class.getName()); + private final Context mContext = getInstrumentation().getTargetContext(); + private final InputMethodManager mInputMethodManager = + mContext.getSystemService(InputMethodManager.class); + private final UiAutomation mUiAutomation = getInstrumentation().getUiAutomation(); private ActivityScenario<MainActivity> mActivityScenario; private MainActivity mActivity; @@ -69,17 +90,19 @@ public final class ConcurrentMultiUserTest { // Launch driver activity. mActivityScenario = ActivityScenario.launch(MainActivity.class); mActivityScenario.onActivity(activity -> mActivity = activity); + mUiAutomation.adoptShellPermissionIdentity(INTERACT_ACROSS_USERS_FULL); } @After public void tearDown() { + mUiAutomation.dropShellPermissionIdentity(); if (mActivityScenario != null) { mActivityScenario.close(); } } @Test - public void driverShowImeNotAffectPassenger() { + public void driverShowImeNotAffectPassenger() throws Exception { assertDriverImeHidden(); assertPassengerImeHidden(); @@ -87,6 +110,91 @@ public final class ConcurrentMultiUserTest { assertPassengerImeHidden(); } + @Test + @Ignore("b/352823913") + public void passengerShowImeNotAffectDriver() throws Exception { + assertDriverImeHidden(); + assertPassengerImeHidden(); + + showPassengerImeAndAssert(); + assertDriverImeHidden(); + } + + @Test + public void driverHideImeNotAffectPassenger() throws Exception { + showDriverImeAndAssert(); + showPassengerImeAndAssert(); + + hideDriverImeAndAssert(); + assertPassengerImeShown(); + } + + @Test + public void passengerHideImeNotAffectDriver() throws Exception { + showDriverImeAndAssert(); + showPassengerImeAndAssert(); + + hidePassengerImeAndAssert(); + assertDriverImeShown(); + } + + @Test + public void imeListNotEmpty() { + List<InputMethodInfo> driverImeList = mInputMethodManager.getInputMethodList(); + assertWithMessage("Driver IME list shouldn't be empty") + .that(driverImeList.isEmpty()).isFalse(); + + List<InputMethodInfo> passengerImeList = + mInputMethodManager.getInputMethodListAsUser(mPeerUserId); + assertWithMessage("Passenger IME list shouldn't be empty") + .that(passengerImeList.isEmpty()).isFalse(); + } + + @Test + public void enabledImeListNotEmpty() { + List<InputMethodInfo> driverEnabledImeList = + mInputMethodManager.getEnabledInputMethodList(); + assertWithMessage("Driver enabled IME list shouldn't be empty") + .that(driverEnabledImeList.isEmpty()).isFalse(); + + List<InputMethodInfo> passengerEnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(mPeerUserId)); + assertWithMessage("Passenger enabled IME list shouldn't be empty") + .that(passengerEnabledImeList.isEmpty()).isFalse(); + } + + @Test + public void currentImeNotNull() { + InputMethodInfo driverIme = mInputMethodManager.getCurrentInputMethodInfo(); + assertWithMessage("Driver IME shouldn't be null").that(driverIme).isNotNull(); + + InputMethodInfo passengerIme = + mInputMethodManager.getCurrentInputMethodInfoAsUser(UserHandle.of(mPeerUserId)); + assertWithMessage("Passenger IME shouldn't be null") + .that(passengerIme).isNotNull(); + } + + @Test + public void enableDisableImePerUser() throws IOException { + UserHandle driver = UserHandle.of(mContext.getUserId()); + UserHandle passenger = UserHandle.of(mPeerUserId); + enableDisableImeForUser(driver, passenger); + enableDisableImeForUser(passenger, driver); + } + + @Test + public void setImePerUser() throws IOException { + UserHandle driver = UserHandle.of(mContext.getUserId()); + UserHandle passenger = UserHandle.of(mPeerUserId); + setImeForUser(driver, passenger); + setImeForUser(passenger, driver); + } + + private void assertDriverImeShown() { + assertWithMessage("Driver IME should be shown") + .that(mActivity.isMyImeVisible()).isTrue(); + } + private void assertDriverImeHidden() { assertWithMessage("Driver IME should be hidden") .that(mActivity.isMyImeVisible()).isFalse(); @@ -98,10 +206,157 @@ public final class ConcurrentMultiUserTest { Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), mPeerUserId, bundleToSend); assertWithMessage("Passenger IME should be hidden") - .that(receivedBundle.getInt(KEY_RESULT_CODE)).isEqualTo(REPLY_IME_HIDDEN); + .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse(); } - private void showDriverImeAndAssert() { + private void assertPassengerImeShown() { + final Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_IME_STATUS); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + assertWithMessage("Passenger IME should be shown") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue(); + } + + private void showDriverImeAndAssert() throws Exception { + // WindowManagerInternal only allows the top focused display to show IME, so this method + // taps the driver display in case it is not the top focused display. + moveDriverDisplayToTop(); + mActivity.showMyImeAndWait(); } + + private void hideDriverImeAndAssert() { + mActivity.hideMyImeAndWait(); + } + + private void showPassengerImeAndAssert() throws Exception { + // WindowManagerInternal only allows the top focused display to show IME, so this method + // taps the passenger display in case it is not the top focused display. + movePassengerDisplayToTop(); + + Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_SHOW_IME); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + + assertWithMessage("Passenger IME should be shown") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN)).isTrue(); + } + + private void hidePassengerImeAndAssert() { + Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_HIDE_IME); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + + assertWithMessage("Passenger IME should be hidden") + .that(receivedBundle.getBoolean(KEY_IME_SHOWN, /* defaultValue= */ true)).isFalse(); + } + + private void moveDriverDisplayToTop() throws Exception { + float[] driverEditTextCenter = mActivity.getEditTextCenter(); + SystemUtil.runShellCommand(mUiAutomation, String.format("input tap %f %f", + driverEditTextCenter[0], driverEditTextCenter[1])); + } + + private void movePassengerDisplayToTop() throws Exception { + final Bundle bundleToSend = new Bundle(); + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_EDITTEXT_POSITION); + Bundle receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + final float[] passengerEditTextCenter = receivedBundle.getFloatArray(KEY_EDITTEXT_CENTER); + + bundleToSend.putInt(KEY_REQUEST_CODE, REQUEST_DISPLAY_ID); + receivedBundle = sendBundleAndWaitForReply(TEST_ACTIVITY.getPackageName(), + mPeerUserId, bundleToSend); + final int passengerDisplayId = receivedBundle.getInt(KEY_DISPLAY_ID); + SystemUtil.runShellCommand(mUiAutomation, String.format("input -d %d tap %f %f", + passengerDisplayId, passengerEditTextCenter[0], passengerEditTextCenter[1])); + } + + /** + * Disables/enables IME for {@code user1}, then verifies that the IME settings for {@code user1} + * has changed as expected and {@code user2} stays the same. + */ + private void enableDisableImeForUser(UserHandle user1, UserHandle user2) throws IOException { + List<InputMethodInfo> user1EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + + // Disable an IME for user1. + InputMethodInfo imeToDisable = user1EnabledImeList.get(0); + SystemUtil.runShellCommand(mUiAutomation, + "ime disable --user " + user1.getIdentifier() + " " + imeToDisable.getId()); + List<InputMethodInfo> user1EnabledImeList2 = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList2 = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId() + + " should be disabled") + .that(user1EnabledImeList2.contains(imeToDisable)).isFalse(); + assertWithMessage("Disabling user " + user1.getIdentifier() + + " IME shouldn't affect user " + user2.getIdentifier()) + .that(user2EnabledImeList2.containsAll(user2EnabledImeList) + && user2EnabledImeList.containsAll(user2EnabledImeList2)) + .isTrue(); + + // Enable the IME. + SystemUtil.runShellCommand(mUiAutomation, + "ime enable --user " + user1.getIdentifier() + " " + imeToDisable.getId()); + List<InputMethodInfo> user1EnabledImeList3 = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + List<InputMethodInfo> user2EnabledImeList3 = + mInputMethodManager.getEnabledInputMethodListAsUser(user2); + assertWithMessage("User " + user1.getIdentifier() + " IME " + imeToDisable.getId() + + " should be enabled").that(user1EnabledImeList3.contains(imeToDisable)).isTrue(); + assertWithMessage("Enabling user " + user1.getIdentifier() + + " IME shouldn't affect user " + user2.getIdentifier()) + .that(user2EnabledImeList2.containsAll(user2EnabledImeList3) + && user2EnabledImeList3.containsAll(user2EnabledImeList2)) + .isTrue(); + } + + /** + * Sets/resets IME for {@code user1}, then verifies that the IME settings for {@code user1} + * has changed as expected and {@code user2} stays the same. + */ + private void setImeForUser(UserHandle user1, UserHandle user2) throws IOException { + // Reset IME for user1. + SystemUtil.runShellCommand(mUiAutomation, + "ime reset --user " + user1.getIdentifier()); + + List<InputMethodInfo> user1EnabledImeList = + mInputMethodManager.getEnabledInputMethodListAsUser(user1); + assumeTrue("There must be at least two IME to test", user1EnabledImeList.size() >= 2); + InputMethodInfo user1Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + + // Set to another IME for user1. + InputMethodInfo anotherIme = null; + for (InputMethodInfo info : user1EnabledImeList) { + if (!info.equals(user1Ime)) { + anotherIme = info; + } + } + SystemUtil.runShellCommand(mUiAutomation, + "ime set --user " + user1.getIdentifier() + " " + anotherIme.getId()); + InputMethodInfo user1Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime2 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong") + .that(user1Ime2).isEqualTo(anotherIme); + assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change") + .that(user2Ime2).isEqualTo(user2Ime); + + // Reset IME for user1. + SystemUtil.runShellCommand(mUiAutomation, + "ime reset --user " + user1.getIdentifier()); + InputMethodInfo user1Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user1); + InputMethodInfo user2Ime3 = mInputMethodManager.getCurrentInputMethodInfoAsUser(user2); + assertWithMessage("The current IME for user " + user1.getIdentifier() + " is wrong") + .that(user1Ime3).isEqualTo(user1Ime); + assertWithMessage("The current IME for user " + user2.getIdentifier() + " shouldn't change") + .that(user2Ime3).isEqualTo(user2Ime); + } } diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java index f1260008ca59..fa0aa19a8822 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/MainActivity.java @@ -16,20 +16,25 @@ package com.android.server.inputmethod.multisessiontest; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_EDITTEXT_CENTER; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_IME_SHOWN; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_REQUEST_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.KEY_RESULT_CODE; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_HIDDEN; -import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REPLY_IME_SHOWN; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_DISPLAY_ID; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_EDITTEXT_POSITION; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_HIDE_IME; import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_IME_STATUS; +import static com.android.server.inputmethod.multisessiontest.TestRequestConstants.REQUEST_SHOW_IME; import android.app.Activity; import android.os.Bundle; import android.os.Process; import android.util.Log; -import android.view.inputmethod.InputMethodManager; import android.widget.EditText; +import androidx.annotation.WorkerThread; import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; import androidx.core.view.WindowInsetsCompat; import com.android.compatibility.common.util.PollingCheck; @@ -43,7 +48,6 @@ public final class MainActivity extends ConcurrentUserActivityBase { private static final long WAIT_IME_TIMEOUT_MS = 3000; private EditText mEditor; - private InputMethodManager mImm; @Override protected void onCreate(Bundle savedInstanceState) { @@ -52,19 +56,56 @@ public final class MainActivity extends ConcurrentUserActivityBase { + Process.myUserHandle().getIdentifier() + " on display " + getDisplay().getDisplayId()); setContentView(R.layout.main_activity); - mImm = getSystemService(InputMethodManager.class); mEditor = requireViewById(R.id.edit_text); } @Override + protected void onResume() { + super.onResume(); + Log.v(TAG, "onResume"); + } + + @Override + protected void onPause() { + super.onPause(); + Log.v(TAG, "onPause"); + } + + @Override + protected void onStop() { + super.onStop(); + Log.v(TAG, "onResume"); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + Log.v(TAG, "onWindowFocusChanged " + hasFocus); + } + + @Override + @WorkerThread protected Bundle onBundleReceived(Bundle receivedBundle) { final int requestCode = receivedBundle.getInt(KEY_REQUEST_CODE); Log.v(TAG, "onBundleReceived() with request code:" + requestCode); final Bundle replyBundle = new Bundle(); switch (requestCode) { case REQUEST_IME_STATUS: - replyBundle.putInt(KEY_RESULT_CODE, - isMyImeVisible() ? REPLY_IME_SHOWN : REPLY_IME_HIDDEN); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_SHOW_IME: + showMyImeAndWait(); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_HIDE_IME: + hideMyImeAndWait(); + replyBundle.putBoolean(KEY_IME_SHOWN, isMyImeVisible()); + break; + case REQUEST_EDITTEXT_POSITION: + replyBundle.putFloatArray(KEY_EDITTEXT_CENTER, getEditTextCenter()); + break; + case REQUEST_DISPLAY_ID: + replyBundle.putInt(KEY_DISPLAY_ID, getDisplay().getDisplayId()); break; default: throw new RuntimeException("Received undefined request code:" + requestCode); @@ -77,21 +118,41 @@ public final class MainActivity extends ConcurrentUserActivityBase { return insets == null ? false : insets.isVisible(WindowInsetsCompat.Type.ime()); } + float[] getEditTextCenter() { + final float editTextCenterX = mEditor.getX() + 0.5f * mEditor.getWidth(); + final float editTextCenterY = mEditor.getY() + 0.5f * mEditor.getHeight(); + return new float[]{editTextCenterX, editTextCenterY}; + } + + @WorkerThread void showMyImeAndWait() { - Log.v(TAG, "showSoftInput"); runOnUiThread(() -> { - // requestFocus() must run on UI thread. + // View#requestFocus() and WindowInsetsControllerCompat#show() must run on UI thread. if (!mEditor.requestFocus()) { Log.e(TAG, "Failed to focus on mEditor"); return; } - if (!mImm.showSoftInput(mEditor, /* flags= */ 0)) { - Log.e(TAG, String.format("Failed to show my IME as user %d, " - + "mEditor:focused=%b,hasWindowFocus=%b", getUserId(), - mEditor.isFocused(), mEditor.hasWindowFocus())); - } + // Compared to mImm.showSoftInput(), the call below is the recommended way to show the + // keyboard because it is guaranteed to be scheduled after the window is focused. + Log.v(TAG, "showSoftInput"); + WindowCompat.getInsetsController(getWindow(), mEditor).show( + WindowInsetsCompat.Type.ime()); }); PollingCheck.waitFor(WAIT_IME_TIMEOUT_MS, () -> isMyImeVisible(), - String.format("My IME (user %d) didn't show up", getUserId())); + String.format("%s: My IME (user %d) didn't show up", TAG, + Process.myUserHandle().getIdentifier())); + } + + @WorkerThread + void hideMyImeAndWait() { + runOnUiThread(() -> { + Log.v(TAG, "hideSoftInput"); + // WindowInsetsControllerCompat#hide() must run on UI thread. + WindowCompat.getInsetsController(getWindow(), mEditor) + .hide(WindowInsetsCompat.Type.ime()); + }); + PollingCheck.waitFor(WAIT_IME_TIMEOUT_MS, () -> !isMyImeVisible(), + String.format("%s: My IME (user %d) is still shown", TAG, + Process.myUserHandle().getIdentifier())); } } diff --git a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java index 1501bfb69c92..68c9d5403c0b 100644 --- a/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java +++ b/tests/inputmethod/ConcurrentMultiSessionImeTest/src/com/android/server/inputmethod/multisessiontest/TestRequestConstants.java @@ -21,9 +21,13 @@ final class TestRequestConstants { } public static final String KEY_REQUEST_CODE = "key_request_code"; - public static final String KEY_RESULT_CODE = "key_result_code"; + public static final String KEY_EDITTEXT_CENTER = "key_edittext_center"; + public static final String KEY_DISPLAY_ID = "key_display_id"; + public static final String KEY_IME_SHOWN = "key_ime_shown"; public static final int REQUEST_IME_STATUS = 1; - public static final int REPLY_IME_SHOWN = 2; - public static final int REPLY_IME_HIDDEN = 3; + public static final int REQUEST_SHOW_IME = 2; + public static final int REQUEST_HIDE_IME = 3; + public static final int REQUEST_EDITTEXT_POSITION = 4; + public static final int REQUEST_DISPLAY_ID = 5; } diff --git a/tests/testables/src/android/animation/AnimatorTestRule.java b/tests/testables/src/android/animation/AnimatorTestRule.java new file mode 100644 index 000000000000..3b39e1fc6bc7 --- /dev/null +++ b/tests/testables/src/android/animation/AnimatorTestRule.java @@ -0,0 +1,378 @@ +/* + * Copyright (C) 2023 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 android.animation; + +import android.animation.AnimationHandler.AnimationFrameCallback; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Looper; +import android.os.SystemClock; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunnableWithException; +import android.util.AndroidRuntimeException; +import android.util.Singleton; +import android.view.Choreographer; +import android.view.animation.AnimationUtils; + +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.internal.util.Preconditions; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * JUnit {@link TestRule} that can be used to run {@link Animator}s without actually waiting for the + * duration of the animation. This also helps the test to be written in a deterministic manner. + * + * Create an instance of {@code AnimatorTestRule} and specify it as a {@link org.junit.Rule} + * of the test class. Use {@link #advanceTimeBy(long)} to advance animators that have been started. + * Note that {@link #advanceTimeBy(long)} should be called from the same thread you have used to + * start the animator. + * + * <pre> + * {@literal @}SmallTest + * {@literal @}RunWith(AndroidJUnit4.class) + * public class SampleAnimatorTest { + * + * {@literal @}Rule + * public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + * + * {@literal @}UiThreadTest + * {@literal @}Test + * public void sample() { + * final ValueAnimator animator = ValueAnimator.ofInt(0, 1000); + * animator.setDuration(1000L); + * assertThat(animator.getAnimatedValue(), is(0)); + * animator.start(); + * mAnimatorTestRule.advanceTimeBy(500L); + * assertThat(animator.getAnimatedValue(), is(500)); + * } + * } + * </pre> + */ +public final class AnimatorTestRule implements TestRule { + + private final Object mLock = new Object(); + private final Singleton<TestHandler> mTestHandler = new Singleton<>() { + @Override + protected TestHandler create() { + return new TestHandler(); + } + }; + private final Object mTest; + private final long mStartTime; + private long mTotalTimeDelta = 0; + private volatile boolean mCanLockAnimationClock; + private Looper mLooperWithLockedAnimationClock; + + /** + * Construct an AnimatorTestRule with access to the test instance and a custom start time. + * @see #AnimatorTestRule(Object) + */ + public AnimatorTestRule(Object test, long startTime) { + mTest = test; + mStartTime = startTime; + } + + /** + * Construct an AnimatorTestRule for the given test instance with a start time of + * {@link SystemClock#uptimeMillis()}. Initializing the start time with this clock reduces the + * discrepancies with various internals of classes like ValueAnimator which can sometimes read + * that clock via {@link android.view.animation.AnimationUtils#currentAnimationTimeMillis()}. + * + * @param test the test instance used to access the {@link TestableLooper} used by the class. + */ + public AnimatorTestRule(Object test) { + this(test, SystemClock.uptimeMillis()); + } + + @NonNull + @Override + public Statement apply(@NonNull final Statement base, @NonNull Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + final TestHandler testHandler = mTestHandler.get(); + final AnimationHandler objAtStart = AnimationHandler.setTestHandler(testHandler); + final RunnableWithException lockClock = + wrapWithRunBlocking(new LockAnimationClockRunnable()); + final RunnableWithException unlockClock = + wrapWithRunBlocking(new UnlockAnimationClockRunnable()); + try { + lockClock.run(); + base.evaluate(); + } finally { + unlockClock.run(); + AnimationHandler objAtEnd = AnimationHandler.setTestHandler(objAtStart); + if (testHandler != objAtEnd) { + // pass or fail, inner logic not restoring the handler needs to be reported. + // noinspection ThrowFromFinallyBlock + throw new IllegalStateException("Test handler was altered: expected=" + + testHandler + " actual=" + objAtEnd); + } + } + } + }; + } + + private RunnableWithException wrapWithRunBlocking(RunnableWithException runnable) { + RunnableWithException wrapped = TestableLooper.wrapWithRunBlocking(mTest, runnable); + if (wrapped != null) { + return wrapped; + } + return () -> runOnMainThrowing(runnable); + } + + private static void runOnMainThrowing(RunnableWithException runnable) throws Exception { + if (Looper.myLooper() == Looper.getMainLooper()) { + runnable.run(); + } else { + final Throwable[] throwableBox = new Throwable[1]; + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + try { + runnable.run(); + } catch (Throwable t) { + throwableBox[0] = t; + } + }); + if (throwableBox[0] == null) { + return; + } else if (throwableBox[0] instanceof RuntimeException ex) { + throw ex; + } else if (throwableBox[0] instanceof Error err) { + throw err; + } else { + throw new RuntimeException(throwableBox[0]); + } + } + } + + private class LockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mLooperWithLockedAnimationClock = Looper.myLooper(); + mCanLockAnimationClock = true; + lockAnimationClockToCurrentTime(); + } + } + + private class UnlockAnimationClockRunnable implements RunnableWithException { + @Override + public void run() { + mCanLockAnimationClock = false; + mLooperWithLockedAnimationClock = null; + AnimationUtils.unlockAnimationClock(); + } + } + + private void lockAnimationClockToCurrentTime() { + if (!mCanLockAnimationClock) { + throw new AssertionError("Unable to lock the animation clock; " + + "has the test started? already finished?"); + } + if (mLooperWithLockedAnimationClock != Looper.myLooper()) { + throw new AssertionError("Animation clock being locked on " + Looper.myLooper() + + " but should only be locked on " + mLooperWithLockedAnimationClock); + } + long desiredTime = getCurrentTime(); + AnimationUtils.lockAnimationClock(desiredTime); + if (!mCanLockAnimationClock) { + AnimationUtils.unlockAnimationClock(); + throw new AssertionError("Threading error when locking the animation clock"); + } + long outputTime = AnimationUtils.currentAnimationTimeMillis(); + if (outputTime != desiredTime) { + // Skip the test (rather than fail it) if there's a clock issue + throw new AssumptionViolatedException("currentAnimationTimeMillis() is " + outputTime + + " after locking to " + desiredTime); + } + } + + /** + * If any new {@link Animator}s have been registered since the last time the frame time was + * advanced, initialize them with the current frame time. Failing to do this will result in the + * animations beginning on the *next* advancement instead, so this is done automatically for + * test authors inside of {@link #advanceTimeBy}. However this is exposed in case authors want + * to validate operations performed by onStart listeners. + * <p> + * NOTE: This is only required of the platform ValueAnimator because its start() method calls + * {@link AnimationHandler#addAnimationFrameCallback} BEFORE it calls startAnimation(), so this + * rule can't synchronously trigger the callback at that time. + */ + public void initNewAnimators() { + requireLooper("AnimationTestRule#initNewAnimators()"); + long currentTime = getCurrentTime(); + final TestHandler testHandler = mTestHandler.get(); + List<AnimationFrameCallback> newCallbacks = new ArrayList<>(testHandler.mNewCallbacks); + testHandler.mNewCallbacks.clear(); + for (AnimationFrameCallback newCallback : newCallbacks) { + newCallback.doAnimationFrame(currentTime); + } + } + + /** + * Advances the animation clock by the given amount of delta in milliseconds. This call will + * produce an animation frame to all the ongoing animations. This method needs to be + * called on the same thread as {@link Animator#start()}. + * + * @param timeDelta the amount of milliseconds to advance + */ + public void advanceTimeBy(long timeDelta) { + advanceTimeBy(timeDelta, null); + } + + /** + * Advances the animation clock by the given amount of delta in milliseconds. This call will + * produce an animation frame to all the ongoing animations. This method needs to be + * called on the same thread as {@link Animator#start()}. + * <p> + * This method is not for test authors, but for rule authors to ensure that multiple animators + * can be advanced in sync. + * + * @param timeDelta the amount of milliseconds to advance + * @param preFrameAction a consumer to be passed the timeDelta following the time advancement + * but prior to the frame production. + */ + public void advanceTimeBy(long timeDelta, @Nullable Consumer<Long> preFrameAction) { + Preconditions.checkArgumentNonnegative(timeDelta, "timeDelta must not be negative"); + requireLooper("AnimationTestRule#advanceTimeBy(long)"); + final TestHandler testHandler = mTestHandler.get(); + if (timeDelta == 0) { + // If time is not being advanced, all animators will get a tick; don't double tick these + testHandler.mNewCallbacks.clear(); + } else { + // before advancing time, start new animators with the current time + initNewAnimators(); + } + synchronized (mLock) { + // advance time + mTotalTimeDelta += timeDelta; + } + lockAnimationClockToCurrentTime(); + if (preFrameAction != null) { + preFrameAction.accept(timeDelta); + // After letting other code run, clear any new callbacks to avoid double-ticking them + testHandler.mNewCallbacks.clear(); + } + // produce a frame + testHandler.doFrame(); + } + + /** + * Returns the current time in milliseconds tracked by AnimationHandler. Note that this is a + * different time than the time tracked by {@link SystemClock} This method needs to be called on + * the same thread as {@link Animator#start()}. + */ + public long getCurrentTime() { + requireLooper("AnimationTestRule#getCurrentTime()"); + synchronized (mLock) { + return mStartTime + mTotalTimeDelta; + } + } + + private static void requireLooper(String method) { + if (Looper.myLooper() == null) { + throw new AndroidRuntimeException(method + " may only be called on Looper threads"); + } + } + + private class TestHandler extends AnimationHandler { + public final TestProvider mTestProvider = new TestProvider(); + private final List<AnimationFrameCallback> mNewCallbacks = new ArrayList<>(); + + TestHandler() { + setProvider(mTestProvider); + } + + public void doFrame() { + mTestProvider.animateFrame(); + mTestProvider.commitFrame(); + } + + @Override + public void addAnimationFrameCallback(AnimationFrameCallback callback, long delay) { + // NOTE: using the delay is infeasible because the AnimationHandler uses + // SystemClock.uptimeMillis(); -- If we fix this to use an overridable method, then we + // could fix this for tests. + super.addAnimationFrameCallback(callback, 0); + if (delay <= 0) { + mNewCallbacks.add(callback); + } + } + + @Override + public void removeCallback(AnimationFrameCallback callback) { + super.removeCallback(callback); + mNewCallbacks.remove(callback); + } + } + + private class TestProvider implements AnimationHandler.AnimationFrameCallbackProvider { + private long mFrameDelay = 10; + private Choreographer.FrameCallback mFrameCallback = null; + private final List<Runnable> mCommitCallbacks = new ArrayList<>(); + + public void animateFrame() { + Choreographer.FrameCallback frameCallback = mFrameCallback; + mFrameCallback = null; + if (frameCallback != null) { + frameCallback.doFrame(getFrameTime()); + } + } + + public void commitFrame() { + List<Runnable> commitCallbacks = new ArrayList<>(mCommitCallbacks); + mCommitCallbacks.clear(); + for (Runnable commitCallback : commitCallbacks) { + commitCallback.run(); + } + } + + @Override + public void postFrameCallback(Choreographer.FrameCallback callback) { + assert mFrameCallback == null; + mFrameCallback = callback; + } + + @Override + public void postCommitCallback(Runnable runnable) { + mCommitCallbacks.add(runnable); + } + + @Override + public void setFrameDelay(long delay) { + mFrameDelay = delay; + } + + @Override + public long getFrameDelay() { + return mFrameDelay; + } + + @Override + public long getFrameTime() { + return getCurrentTime(); + } + } +} diff --git a/tests/testables/tests/Android.bp b/tests/testables/tests/Android.bp index 06449e0ce574..d6a4754c37aa 100644 --- a/tests/testables/tests/Android.bp +++ b/tests/testables/tests/Android.bp @@ -26,14 +26,18 @@ android_test { platform_apis: true, srcs: [ "src/**/*.java", + "src/**/*.kt", "src/**/I*.aidl", ], resource_dirs: ["res"], static_libs: [ + "androidx.core_core-animation", + "androidx.core_core-ktx", "androidx.test.rules", "hamcrest-library", "mockito-target-inline-minus-junit4", "testables", + "truth", ], compile_multilib: "both", jni_libs: [ diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt new file mode 100644 index 000000000000..5abebee77d3d --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleIsolationTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 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 android.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.core.animation.doOnEnd +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** + * This test class validates that two tests' animators are isolated from each other when using the + * same animator test rule. This is a test to prevent future instances of b/275602127. + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper +class AnimatorTestRuleIsolationTest { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + @Test + fun testA() { + // GIVEN global state is reset at the start of the test + didTouchA = false + didTouchB = false + // WHEN starting 2 animations of different durations, and setting didTouch{A,B} at the end + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchA = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchB = true } + start() + } + // WHEN when you advance time so that only one of the animations has ended + animatorTestRule.advanceTimeBy(100) + // VERIFY we did indeed end the current animation + assertThat(didTouchA).isTrue() + // VERIFY advancing the animator did NOT cause testB's animator to end + assertThat(didTouchB).isFalse() + } + + @Test + fun testB() { + // GIVEN global state is reset at the start of the test + didTouchA = false + didTouchB = false + // WHEN starting 2 animations of different durations, and setting didTouch{A,B} at the end + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 100 + doOnEnd { didTouchB = true } + start() + } + ObjectAnimator.ofFloat(0f, 1f).apply { + duration = 150 + doOnEnd { didTouchA = true } + start() + } + animatorTestRule.advanceTimeBy(100) + // VERIFY advancing the animator did NOT cause testA's animator to end + assertThat(didTouchA).isFalse() + // VERIFY we did indeed end the current animation + assertThat(didTouchB).isTrue() + } + + companion object { + var didTouchA = false + var didTouchB = false + } +} diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt new file mode 100644 index 000000000000..9eeaad5cd272 --- /dev/null +++ b/tests/testables/tests/src/android/animation/AnimatorTestRulePrecisionTest.kt @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2023 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 android.animation + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.animation.LinearInterpolator +import androidx.core.animation.doOnEnd +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidTestingRunner::class) +@SmallTest +@RunWithLooper +class AnimatorTestRulePrecisionTest { + + @get:Rule val animatorTestRule = AnimatorTestRule(this) + + var value1: Float = -1f + var value2: Float = -1f + + private inline fun animateThis( + propertyName: String, + duration: Long, + startDelay: Long = 0, + crossinline onEndAction: (animator: Animator) -> Unit, + ) { + ObjectAnimator.ofFloat(this, propertyName, 0f, 1f).also { + it.interpolator = LINEAR_INTERPOLATOR + it.duration = duration + it.startDelay = startDelay + it.doOnEnd(onEndAction) + it.start() + } + } + + @Test + fun testSingleAnimator() { + var ended = false + animateThis("value1", duration = 100) { ended = true } + + assertThat(value1).isEqualTo(0f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(50) + assertThat(value1).isEqualTo(0.5f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(49) + assertThat(value1).isEqualTo(0.99f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(ended).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testDelayedAnimator() { + var ended = false + animateThis("value1", duration = 100, startDelay = 50) { ended = true } + + assertThat(value1).isEqualTo(-1f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(49) + assertThat(value1).isEqualTo(-1f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(0f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(ended).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(ended).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testTwoAnimators() { + var ended1 = false + var ended2 = false + animateThis("value1", duration = 100) { ended1 = true } + animateThis("value2", duration = 200) { ended2 = true } + assertThat(value1).isEqualTo(0f) + assertThat(value2).isEqualTo(0f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(value2).isEqualTo(0.495f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(2) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.5f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.995f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(1f) + assertThat(ended1).isTrue() + assertThat(ended2).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + @Test + fun testChainedAnimators() { + var ended1 = false + var ended2 = false + animateThis("value1", duration = 100) { + ended1 = true + animateThis("value2", duration = 100) { ended2 = true } + } + + assertThat(value1).isEqualTo(0f) + assertThat(value2).isEqualTo(-1f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(0.99f) + assertThat(value2).isEqualTo(-1f) + assertThat(ended1).isFalse() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(99) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(0.99f) + assertThat(ended1).isTrue() + assertThat(ended2).isFalse() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(1) + + animatorTestRule.advanceTimeBy(1) + assertThat(value1).isEqualTo(1f) + assertThat(value2).isEqualTo(1f) + assertThat(ended1).isTrue() + assertThat(ended2).isTrue() + assertThat(AnimationHandler.getAnimationCount()).isEqualTo(0) + } + + private companion object { + private val LINEAR_INTERPOLATOR = LinearInterpolator() + } +} |