diff options
author | Xin Li <delphij@google.com> | 2023-10-05 15:40:35 -0700 |
---|---|---|
committer | Xin Li <delphij@google.com> | 2023-10-05 15:40:35 -0700 |
commit | 65bef66686f9ea478626d815d565653cb8802813 (patch) | |
tree | 1e55e142ec3bec78e08830866c43f6d09d0ab7f6 | |
parent | bcc6db40f684f8507798d0ec0795c9b9c1951dbe (diff) | |
parent | 6a3b93c3539f8774653d5a0b08890ae8b2cfd72b (diff) | |
download | systemui-65bef66686f9ea478626d815d565653cb8802813.tar.gz |
Merge Android 14
Bug: 298295554
Merged-In: If8dc1d32d7fd02e41c12cfb1571374c1e974e87c
Change-Id: Ibab276d3fe90ca44dd682f5c5912ba958fbec336
47 files changed, 1227 insertions, 996 deletions
diff --git a/animationlib/Android.bp b/animationlib/Android.bp new file mode 100644 index 0000000..d67a5de --- /dev/null +++ b/animationlib/Android.bp @@ -0,0 +1,59 @@ +// Copyright (C) 2018 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + + +android_library { + name: "animationlib", + manifest: "AndroidManifest.xml", + sdk_version: "system_current", + min_sdk_version: "26", + static_libs: [ + "androidx.core_core-animation", + "androidx.core_core-ktx", + "androidx.annotation_annotation", + ], + srcs: [ + "src/**/*.java", + "src/**/*.kt" + ], + kotlincflags: ["-Xjvm-default=all"], + // This library is meant to access only public APIs + // do not flip this flag to true + platform_apis: false +} + +android_test { + name: "animationlib_tests", + manifest: "tests/AndroidManifest.xml", + + static_libs: [ + "animationlib", + "androidx.test.ext.junit", + "androidx.test.rules", + "testables", + ], + libs: [ + "android.test.base", + ], + srcs: [ + "**/*.java", + "**/*.kt" + ], + kotlincflags: ["-Xjvm-default=all"], + test_suites: ["general-tests"], +} diff --git a/searchuilib/AndroidManifest.xml b/animationlib/AndroidManifest.xml index 6c6c5f6..b05fb11 100644 --- a/searchuilib/AndroidManifest.xml +++ b/animationlib/AndroidManifest.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - Copyright (C) 2020 The Android Open Source Project + 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. @@ -16,5 +16,5 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.app.search"> + package="com.android.app.animation"> </manifest> diff --git a/animationlib/TEST_MAPPING b/animationlib/TEST_MAPPING new file mode 100644 index 0000000..4fd6f09 --- /dev/null +++ b/animationlib/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "presubmit": [ + { + "name": "animationlib_tests" + } + ] +} diff --git a/animationlib/build.gradle b/animationlib/build.gradle new file mode 100644 index 0000000..f9c4485 --- /dev/null +++ b/animationlib/build.gradle @@ -0,0 +1,46 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + namespace = "com.android.app.animation" + testNamespace = "com.android.app.animation.tests" + defaultConfig { + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + sourceSets { + main { + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + } + androidTest { + java.srcDirs = ["tests"] + } + } + compileSdk 33 + + defaultConfig { + minSdk 33 + targetSdk 33 + } + + lintOptions { + abortOnError false + } + tasks.lint.enabled = false + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } + kotlinOptions { + jvmTarget = '1.8' + freeCompilerArgs = ["-Xjvm-default=all"] + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0" + implementation "androidx.core:core-animation:1.0.0-alpha02" + implementation "androidx.core:core-ktx:1.9.0" + androidTestImplementation "androidx.test.ext:junit:1.1.3" + androidTestImplementation "androidx.test:rules:1.4.0" +} diff --git a/animationlib/src/com/android/app/animation/Interpolators.java b/animationlib/src/com/android/app/animation/Interpolators.java new file mode 100644 index 0000000..0f3776c --- /dev/null +++ b/animationlib/src/com/android/app/animation/Interpolators.java @@ -0,0 +1,211 @@ +/* + * 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 com.android.app.animation; + +import android.graphics.Path; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.BounceInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; + +/** + * Utility class to receive interpolators from. + * + * Make sure that changes made to this class are also reflected in {@link InterpolatorsAndroidX}. + * Please consider using the androidx dependencies featuring better testability altogether. + */ +public class Interpolators { + + /* + * ============================================================================================ + * Emphasized interpolators. + * ============================================================================================ + */ + + /** + * The default emphasized interpolator. Used for hero / emphasized movement of content. + */ + public static final Interpolator EMPHASIZED = createEmphasizedInterpolator(); + + /** + * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that + * is disappearing e.g. when moving off screen. + */ + public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 0.8f, 0.15f); + + /** + * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that + * is appearing e.g. when coming from off screen + */ + public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( + 0.05f, 0.7f, 0.1f, 1f); + + + /* + * ============================================================================================ + * Standard interpolators. + * ============================================================================================ + */ + + /** + * The standard interpolator that should be used on every normal animation + */ + public static final Interpolator STANDARD = new PathInterpolator( + 0.2f, 0f, 0f, 1f); + + /** + * The standard accelerating interpolator that should be used on every regular movement of + * content that is disappearing e.g. when moving off screen. + */ + public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 1f, 1f); + + /** + * The standard decelerating interpolator that should be used on every regular movement of + * content that is appearing e.g. when coming from off screen. + */ + public static final Interpolator STANDARD_DECELERATE = new PathInterpolator( + 0f, 0f, 0f, 1f); + + /* + * ============================================================================================ + * Legacy + * ============================================================================================ + */ + + /** + * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /** + * The default legacy accelerating interpolator as defined in Material 1. + * Also known as FAST_OUT_LINEAR_IN. + */ + public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f); + + /** + * The default legacy decelerating interpolator as defined in Material 1. + * Also known as LINEAR_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** + * Linear interpolator. Often used if the interpolator is for different properties who need + * different interpolations. + */ + public static final Interpolator LINEAR = new LinearInterpolator(); + + /* + * ============================================================================================ + * Custom interpolators + * ============================================================================================ + */ + + public static final Interpolator FAST_OUT_SLOW_IN = LEGACY; + public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE; + public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE; + + /** + * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t + * goes from 1 to 0 instead of 0 to 1). + */ + public static final Interpolator FAST_OUT_SLOW_IN_REVERSE = + new PathInterpolator(0.8f, 0f, 0.6f, 1f); + public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f); + public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); + public static final Interpolator ACCELERATE = new AccelerateInterpolator(); + public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator(); + public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f); + public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f); + public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f); + public static final Interpolator ICON_OVERSHOT_LESS = new PathInterpolator(0.4f, 0f, 0.2f, + 1.1f); + public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f, + 1); + public static final Interpolator BOUNCE = new BounceInterpolator(); + /** + * For state transitions on the control panel that lives in GlobalActions. + */ + public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f, + 1.0f); + + /** + * Interpolator to be used when animating a move based on a click. Pair with enough duration. + */ + public static final Interpolator TOUCH_RESPONSE = + new PathInterpolator(0.3f, 0f, 0.1f, 1f); + + /** + * Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t + * goes from 1 to 0 instead of 0 to 1). + */ + public static final Interpolator TOUCH_RESPONSE_REVERSE = + new PathInterpolator(0.9f, 0f, 0.7f, 1f); + + /* + * ============================================================================================ + * Functions / Utilities + * ============================================================================================ + */ + + /** + * Calculate the amount of overshoot using an exponential falloff function with desired + * properties, where the overshoot smoothly transitions at the 1.0f boundary into the + * overshoot, retaining its acceleration. + * + * @param progress a progress value going from 0 to 1 + * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max + * value of the overall progress will be at 1.1. + * @param overshootStart the point in (0,1] where the result should reach 1 + * @return the interpolated overshoot + */ + public static float getOvershootInterpolation(float progress, float overshootAmount, + float overshootStart) { + if (overshootAmount == 0.0f || overshootStart == 0.0f) { + throw new IllegalArgumentException("Invalid values for overshoot"); + } + float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart; + return MathUtils.max(0.0f, + (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f)); + } + + /** + * Similar to {@link #getOvershootInterpolation(float, float, float)} but the overshoot + * starts immediately here, instead of first having a section of non-overshooting + * + * @param progress a progress value going from 0 to 1 + */ + public static float getOvershootInterpolation(float progress) { + return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress))); + } + + // Create the default emphasized interpolator + private static PathInterpolator createEmphasizedInterpolator() { + Path path = new Path(); + // Doing the same as fast_out_extra_slow_in + path.moveTo(0f, 0f); + path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f); + path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f); + return new PathInterpolator(path); + } +}
\ No newline at end of file diff --git a/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java b/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java new file mode 100644 index 0000000..7142f54 --- /dev/null +++ b/animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java @@ -0,0 +1,218 @@ +/* + * 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 com.android.app.animation; + +import android.graphics.Path; + +import androidx.core.animation.AccelerateDecelerateInterpolator; +import androidx.core.animation.AccelerateInterpolator; +import androidx.core.animation.BounceInterpolator; +import androidx.core.animation.DecelerateInterpolator; +import androidx.core.animation.Interpolator; +import androidx.core.animation.LinearInterpolator; +import androidx.core.animation.PathInterpolator; + +/** + * Utility class to receive interpolators from. (androidx compatible version) + * + * This is the androidx compatible version of {@link Interpolators}. Make sure that changes made to + * this class are also reflected in {@link Interpolators}. + * + * Using the androidx versions of {@link androidx.core.animation.ValueAnimator} or + * {@link androidx.core.animation.ObjectAnimator} improves animation testability. This file provides + * the androidx compatible versions of the interpolators defined in {@link Interpolators}. + * AnimatorTestRule can be used in Tests to manipulate the animation under test (e.g. artificially + * advancing the time). + */ +public class InterpolatorsAndroidX { + + /* + * ============================================================================================ + * Emphasized interpolators. + * ============================================================================================ + */ + + /** + * The default emphasized interpolator. Used for hero / emphasized movement of content. + */ + public static final Interpolator EMPHASIZED = createEmphasizedInterpolator(); + + /** + * The accelerated emphasized interpolator. Used for hero / emphasized movement of content that + * is disappearing e.g. when moving off screen. + */ + public static final Interpolator EMPHASIZED_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 0.8f, 0.15f); + + /** + * The decelerating emphasized interpolator. Used for hero / emphasized movement of content that + * is appearing e.g. when coming from off screen + */ + public static final Interpolator EMPHASIZED_DECELERATE = new PathInterpolator( + 0.05f, 0.7f, 0.1f, 1f); + + + /* + * ============================================================================================ + * Standard interpolators. + * ============================================================================================ + */ + + /** + * The standard interpolator that should be used on every normal animation + */ + public static final Interpolator STANDARD = new PathInterpolator( + 0.2f, 0f, 0f, 1f); + + /** + * The standard accelerating interpolator that should be used on every regular movement of + * content that is disappearing e.g. when moving off screen. + */ + public static final Interpolator STANDARD_ACCELERATE = new PathInterpolator( + 0.3f, 0f, 1f, 1f); + + /** + * The standard decelerating interpolator that should be used on every regular movement of + * content that is appearing e.g. when coming from off screen. + */ + public static final Interpolator STANDARD_DECELERATE = new PathInterpolator( + 0f, 0f, 0f, 1f); + + /* + * ============================================================================================ + * Legacy + * ============================================================================================ + */ + + /** + * The default legacy interpolator as defined in Material 1. Also known as FAST_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY = new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /** + * The default legacy accelerating interpolator as defined in Material 1. + * Also known as FAST_OUT_LINEAR_IN. + */ + public static final Interpolator LEGACY_ACCELERATE = new PathInterpolator(0.4f, 0f, 1f, 1f); + + /** + * The default legacy decelerating interpolator as defined in Material 1. + * Also known as LINEAR_OUT_SLOW_IN. + */ + public static final Interpolator LEGACY_DECELERATE = new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** + * Linear interpolator. Often used if the interpolator is for different properties who need + * different interpolations. + */ + public static final Interpolator LINEAR = new LinearInterpolator(); + + /* + * ============================================================================================ + * Custom interpolators + * ============================================================================================ + */ + + public static final Interpolator FAST_OUT_SLOW_IN = LEGACY; + public static final Interpolator FAST_OUT_LINEAR_IN = LEGACY_ACCELERATE; + public static final Interpolator LINEAR_OUT_SLOW_IN = LEGACY_DECELERATE; + + /** + * Like {@link #FAST_OUT_SLOW_IN}, but used in case the animation is played in reverse (i.e. t + * goes from 1 to 0 instead of 0 to 1). + */ + public static final Interpolator FAST_OUT_SLOW_IN_REVERSE = + new PathInterpolator(0.8f, 0f, 0.6f, 1f); + public static final Interpolator SLOW_OUT_LINEAR_IN = new PathInterpolator(0.8f, 0f, 1f, 1f); + public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); + public static final Interpolator ALPHA_OUT = new PathInterpolator(0f, 0f, 0.8f, 1f); + public static final Interpolator ACCELERATE = new AccelerateInterpolator(); + public static final Interpolator ACCELERATE_DECELERATE = new AccelerateDecelerateInterpolator(); + public static final Interpolator DECELERATE_QUINT = new DecelerateInterpolator(2.5f); + public static final Interpolator CUSTOM_40_40 = new PathInterpolator(0.4f, 0f, 0.6f, 1f); + public static final Interpolator ICON_OVERSHOT = new PathInterpolator(0.4f, 0f, 0.2f, 1.4f); + public static final Interpolator ICON_OVERSHOT_LESS = new PathInterpolator(0.4f, 0f, 0.2f, + 1.1f); + public static final Interpolator PANEL_CLOSE_ACCELERATED = new PathInterpolator(0.3f, 0, 0.5f, + 1); + public static final Interpolator BOUNCE = new BounceInterpolator(); + /** + * For state transitions on the control panel that lives in GlobalActions. + */ + public static final Interpolator CONTROL_STATE = new PathInterpolator(0.4f, 0f, 0.2f, + 1.0f); + + /** + * Interpolator to be used when animating a move based on a click. Pair with enough duration. + */ + public static final Interpolator TOUCH_RESPONSE = + new PathInterpolator(0.3f, 0f, 0.1f, 1f); + + /** + * Like {@link #TOUCH_RESPONSE}, but used in case the animation is played in reverse (i.e. t + * goes from 1 to 0 instead of 0 to 1). + */ + public static final Interpolator TOUCH_RESPONSE_REVERSE = + new PathInterpolator(0.9f, 0f, 0.7f, 1f); + + /* + * ============================================================================================ + * Functions / Utilities + * ============================================================================================ + */ + + /** + * Calculate the amount of overshoot using an exponential falloff function with desired + * properties, where the overshoot smoothly transitions at the 1.0f boundary into the + * overshoot, retaining its acceleration. + * + * @param progress a progress value going from 0 to 1 + * @param overshootAmount the amount > 0 of overshoot desired. A value of 0.1 means the max + * value of the overall progress will be at 1.1. + * @param overshootStart the point in (0,1] where the result should reach 1 + * @return the interpolated overshoot + */ + public static float getOvershootInterpolation(float progress, float overshootAmount, + float overshootStart) { + if (overshootAmount == 0.0f || overshootStart == 0.0f) { + throw new IllegalArgumentException("Invalid values for overshoot"); + } + float b = MathUtils.log((overshootAmount + 1) / (overshootAmount)) / overshootStart; + return MathUtils.max(0.0f, + (float) (1.0f - Math.exp(-b * progress)) * (overshootAmount + 1.0f)); + } + + /** + * Similar to {@link #getOvershootInterpolation(float, float, float)} but the overshoot + * starts immediately here, instead of first having a section of non-overshooting + * + * @param progress a progress value going from 0 to 1 + */ + public static float getOvershootInterpolation(float progress) { + return MathUtils.max(0.0f, (float) (1.0f - Math.exp(-4 * progress))); + } + + // Create the default emphasized interpolator + private static PathInterpolator createEmphasizedInterpolator() { + Path path = new Path(); + // Doing the same as fast_out_extra_slow_in + path.moveTo(0f, 0f); + path.cubicTo(0.05f, 0f, 0.133333f, 0.06f, 0.166666f, 0.4f); + path.cubicTo(0.208333f, 0.82f, 0.25f, 1f, 1f, 1f); + return new PathInterpolator(path); + } +}
\ No newline at end of file diff --git a/animationlib/src/com/android/app/animation/MathUtils.java b/animationlib/src/com/android/app/animation/MathUtils.java new file mode 100644 index 0000000..d0a34c8 --- /dev/null +++ b/animationlib/src/com/android/app/animation/MathUtils.java @@ -0,0 +1,27 @@ +/* + * 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 com.android.app.animation; + +public final class MathUtils { + public static float log(float a) { + return (float) Math.log(a); + } + + public static float max(float a, float b) { + return a > b ? a : b; + } +} diff --git a/animationlib/tests/AndroidManifest.xml b/animationlib/tests/AndroidManifest.xml new file mode 100644 index 0000000..77a5990 --- /dev/null +++ b/animationlib/tests/AndroidManifest.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.app.animation.tests"> + + <instrumentation + android:name="android.testing.TestableInstrumentation" + android:label="Tests for public Animation Lib" + android:targetPackage="com.android.app.animation.tests"/> + +</manifest> diff --git a/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt b/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt new file mode 100644 index 0000000..841e141 --- /dev/null +++ b/animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt @@ -0,0 +1,54 @@ +/* + * 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 com.android.app.animation + +import androidx.test.filters.SmallTest +import java.lang.reflect.Modifier +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@SmallTest +@RunWith(JUnit4::class) +class InterpolatorsAndroidXTest { + + @Test + fun testInterpolatorsAndInterpolatorsAndroidXPublicMethodsAreEqual() { + assertEquals( + Interpolators::class.java.getPublicMethods(), + InterpolatorsAndroidX::class.java.getPublicMethods() + ) + } + + @Test + fun testInterpolatorsAndInterpolatorsAndroidXPublicFieldsAreEqual() { + assertEquals( + Interpolators::class.java.getPublicFields(), + InterpolatorsAndroidX::class.java.getPublicFields() + ) + } + + private fun <T> Class<T>.getPublicMethods() = + declaredMethods + .filter { Modifier.isPublic(it.modifiers) } + .map { it.toString().replace(name, "") } + .toSet() + + private fun <T> Class<T>.getPublicFields() = + fields.filter { Modifier.isPublic(it.modifiers) }.map { it.name }.toSet() +} diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle index 23c7cb0..344ac20 100644 --- a/iconloaderlib/build.gradle +++ b/iconloaderlib/build.gradle @@ -1,8 +1,9 @@ plugins { - id 'sysuigradleproject.android-library-conventions' + id 'com.android.library' } android { + namespace = "com.android.launcher3.icons" sourceSets { main { java.srcDirs = ['src', 'src_full_lib'] @@ -10,8 +11,7 @@ android { res.srcDirs = ['res'] } } - - lintOptions { + lint { abortOnError false } diff --git a/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml new file mode 100644 index 0000000..3a59e3d --- /dev/null +++ b/iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/profile_badge_size" + android:height="@dimen/profile_badge_size" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#11000000" + android:pathData="M.5,12.25 + A11.5,11.5 0 1,1 23.5,12.25 + A11.5,11.5 0 1,1 .5,12.25" /> + + <path + android:fillColor="@color/themed_icon_background_color" + android:pathData="M1,12 + A11,11 0 1,1 23,12 + A11,11 0 1,1 1,12" /> + + <group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12"> + <path + android:pathData="M22,9.5C22,13.642 18.642,17 14.5,17C10.358,17 7,13.642 7,9.5C7,5.358 10.358,2 14.5,2C18.642,2 22,5.358 22,9.5Z" + android:fillColor="@color/themed_badge_icon_color"/> + <path + android:pathData="M9.5,20.333C12.722,20.333 15.333,17.722 15.333,14.5C15.333,11.278 12.722,8.667 9.5,8.667C6.278,8.667 3.667,11.278 3.667,14.5C3.667,17.722 6.278,20.333 9.5,20.333ZM9.5,22C13.642,22 17,18.642 17,14.5C17,10.358 13.642,7 9.5,7C5.358,7 2,10.358 2,14.5C2,18.642 5.358,22 9.5,22Z" + android:fillColor="@color/themed_badge_icon_color" + android:fillType="evenOdd"/> + </group> +</vector> diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml index b74317e..e6b5701 100644 --- a/iconloaderlib/res/drawable/ic_instant_app_badge.xml +++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml @@ -20,20 +20,11 @@ android:viewportHeight="18"> <path - android:fillColor="@android:color/black" - android:strokeWidth="1" - android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> - <path - android:fillColor="@android:color/white" - android:strokeWidth="1" - android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> - <path android:fillColor="@android:color/white" android:strokeWidth="1" android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> <path android:fillColor="@android:color/black" - android:fillAlpha="0.87" android:strokeWidth="1" android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" /> </vector> diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml new file mode 100644 index 0000000..6e19339 --- /dev/null +++ b/iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2017 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/profile_badge_size" + android:height="@dimen/profile_badge_size" + android:viewportWidth="18" + android:viewportHeight="18"> + + <path + android:fillColor="@color/themed_badge_icon_background_color" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@color/themed_badge_icon_color" + android:strokeWidth="1" + android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" /> +</vector> diff --git a/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml b/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml new file mode 100644 index 0000000..6866d2f --- /dev/null +++ b/iconloaderlib/res/drawable/ic_work_app_badge_themed.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/profile_badge_size" + android:height="@dimen/profile_badge_size" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:fillColor="#11000000" + android:pathData="M.5,12.25 + A11.5,11.5 0 1,1 23.5,12.25 + A11.5,11.5 0 1,1 .5,12.25" /> + + <path + android:fillColor="@color/themed_badge_icon_background_color" + android:pathData="M1,12 + A11,11 0 1,1 23,12 + A11,11 0 1,1 1,12" /> + + <group android:scaleX=".6" android:scaleY=".6" android:pivotX="12" android:pivotY="12"> + <path + android:fillColor="@color/themed_badge_icon_color" + android:pathData="M20,6h-4L16,4c0,-1.11 -0.89,-2 -2,-2h-4c-1.11,0 -2,0.89 -2,2v2L4,6c-1.11,0 -1.99,0.89 -1.99,2L2,19c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM14,6h-4L10,4h4v2z" /> + </group> +</vector> diff --git a/iconloaderlib/res/values-night-v31/colors.xml b/iconloaderlib/res/values-night-v31/colors.xml new file mode 100644 index 0000000..e5ebda6 --- /dev/null +++ b/iconloaderlib/res/values-night-v31/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 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. +*/ +--> +<resources> + <color name="themed_icon_color">@android:color/system_accent1_200</color> + <color name="themed_icon_background_color">@android:color/system_accent2_800</color> + <color name="themed_badge_icon_color">@android:color/system_accent2_800</color> + <color name="themed_badge_icon_background_color">@android:color/system_accent1_200</color> +</resources> diff --git a/iconloaderlib/res/values-night/colors.xml b/iconloaderlib/res/values-night/colors.xml new file mode 100644 index 0000000..9de7074 --- /dev/null +++ b/iconloaderlib/res/values-night/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 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. +*/ +--> +<resources> + <color name="themed_icon_color">#A8C7FA</color> + <color name="themed_icon_background_color">#003355</color> + <color name="themed_badge_icon_color">#003355</color> + <color name="themed_badge_icon_background_color">#A8C7FA</color> +</resources> diff --git a/iconloaderlib/res/values-v31/colors.xml b/iconloaderlib/res/values-v31/colors.xml new file mode 100644 index 0000000..1405ad0 --- /dev/null +++ b/iconloaderlib/res/values-v31/colors.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 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. +*/ +--> +<resources> + <color name="themed_icon_color">@android:color/system_accent1_700</color> + <color name="themed_icon_background_color">@android:color/system_accent1_100</color> + <color name="themed_badge_icon_color">@android:color/system_accent1_700</color> + <color name="themed_badge_icon_background_color">@android:color/system_accent1_100</color> +</resources> diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml index 70582c2..8eeafb4 100644 --- a/iconloaderlib/res/values/colors.xml +++ b/iconloaderlib/res/values/colors.xml @@ -17,6 +17,10 @@ */ --> <resources> + <color name="themed_icon_color">#0842A0</color> + <color name="themed_icon_background_color">#D3E3FD</color> + <color name="themed_badge_icon_color">#0842A0</color> + <color name="themed_badge_icon_background_color">#D3E3FD</color> <color name="legacy_icon_background">#FFFFFF</color> <!-- Yellow 600, used for highlighting "important" conversations in settings & notifications --> diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java index 37ad04d..d1ef6f7 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java +++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java @@ -16,6 +16,7 @@ package com.android.launcher3.icons; import android.content.Context; +import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.Canvas; @@ -154,11 +155,17 @@ public class BitmapInfo { if (badgeInfo != null) { drawable.setBadge(badgeInfo.newIcon(context, creationFlags)); } else if ((flags & FLAG_INSTANT) != 0) { - drawable.setBadge(context.getDrawable(R.drawable.ic_instant_app_badge)); + drawable.setBadge(context.getDrawable(drawable.isThemed() + ? R.drawable.ic_instant_app_badge_themed + : R.drawable.ic_instant_app_badge)); } else if ((flags & FLAG_WORK) != 0) { - drawable.setBadge(context.getDrawable(R.drawable.ic_work_app_badge)); + drawable.setBadge(context.getDrawable(drawable.isThemed() + ? R.drawable.ic_work_app_badge_themed + : R.drawable.ic_work_app_badge)); } else if ((flags & FLAG_CLONE) != 0) { - drawable.setBadge(context.getDrawable(R.drawable.ic_clone_app_badge)); + drawable.setBadge(context.getDrawable(drawable.isThemed() + ? R.drawable.ic_clone_app_badge_themed + : R.drawable.ic_clone_app_badge)); } } } diff --git a/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java new file mode 100644 index 0000000..a4ac812 --- /dev/null +++ b/iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java @@ -0,0 +1,160 @@ +package com.android.launcher3.icons; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.LauncherApps; +import android.content.pm.ShortcutInfo; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.Rect; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Factory for creating normalized bubble icons and app badges. + */ +public class BubbleIconFactory extends BaseIconFactory { + + private final int mRingColor; + private final int mRingWidth; + + private final BaseIconFactory mBadgeFactory; + + /** + * Creates a bubble icon factory. + * + * @param context the context for the factory. + * @param iconSize the size of the bubble icon (i.e. the large icon for the bubble). + * @param badgeSize the size of the badge (i.e. smaller icon shown on top of the large icon). + * @param ringColor the color of the ring optionally shown around the badge. + * @param ringWidth the width of the ring optionally shown around the badge. + */ + public BubbleIconFactory(Context context, int iconSize, int badgeSize, int ringColor, + int ringWidth) { + super(context, context.getResources().getConfiguration().densityDpi, iconSize); + mRingColor = ringColor; + mRingWidth = ringWidth; + + mBadgeFactory = new BaseIconFactory(context, + context.getResources().getConfiguration().densityDpi, + badgeSize); + } + + /** + * Returns the drawable that the developer has provided to display in the bubble. + */ + public Drawable getBubbleDrawable(@NonNull final Context context, + @Nullable final ShortcutInfo shortcutInfo, @Nullable final Icon ic) { + if (shortcutInfo != null) { + LauncherApps launcherApps = context.getSystemService(LauncherApps.class); + int density = context.getResources().getConfiguration().densityDpi; + return launcherApps.getShortcutIconDrawable(shortcutInfo, density); + } else { + if (ic != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (ic.getType() == Icon.TYPE_URI + || ic.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + context.grantUriPermission(context.getPackageName(), + ic.getUri(), + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + return ic.loadDrawable(context); + } + return null; + } + } + + /** + * Creates the bitmap for the provided drawable and returns the scale used for + * drawing the actual drawable. This is used for the larger icon shown for the bubble. + */ + public Bitmap getBubbleBitmap(@NonNull Drawable icon, float[] outScale) { + if (outScale == null) { + outScale = new float[1]; + } + icon = normalizeAndWrapToAdaptiveIcon(icon, + true /* shrinkNonAdaptiveIcons */, + null /* outscale */, + outScale); + return createIconBitmap(icon, outScale[0], MODE_WITH_SHADOW); + } + + /** + * Returns a {@link BitmapInfo} for the app-badge that is shown on top of each bubble. This + * will include the workprofile indicator on the badge if appropriate. + */ + public BitmapInfo getBadgeBitmap(Drawable userBadgedAppIcon, boolean isImportantConversation) { + if (userBadgedAppIcon instanceof AdaptiveIconDrawable) { + AdaptiveIconDrawable ad = (AdaptiveIconDrawable) userBadgedAppIcon; + userBadgedAppIcon = new CircularAdaptiveIcon(ad.getBackground(), + ad.getForeground()); + } + if (isImportantConversation) { + userBadgedAppIcon = new CircularRingDrawable(userBadgedAppIcon); + } + Bitmap userBadgedBitmap = mBadgeFactory.createIconBitmap( + userBadgedAppIcon, 1, MODE_WITH_SHADOW); + return mBadgeFactory.createIconBitmap(userBadgedBitmap); + } + + private class CircularRingDrawable extends CircularAdaptiveIcon { + final Rect mInnerBounds = new Rect(); + + final Drawable mDr; + + CircularRingDrawable(Drawable dr) { + super(null, null); + mDr = dr; + } + + @Override + public void draw(Canvas canvas) { + int save = canvas.save(); + canvas.clipPath(getIconMask()); + canvas.drawColor(mRingColor); + mInnerBounds.set(getBounds()); + mInnerBounds.inset(mRingWidth, mRingWidth); + canvas.translate(mInnerBounds.left, mInnerBounds.top); + mDr.setBounds(0, 0, mInnerBounds.width(), mInnerBounds.height()); + mDr.draw(canvas); + canvas.restoreToCount(save); + } + } + + private static class CircularAdaptiveIcon extends AdaptiveIconDrawable { + + final Path mPath = new Path(); + + CircularAdaptiveIcon(Drawable bg, Drawable fg) { + super(bg, fg); + } + + @Override + public Path getIconMask() { + mPath.reset(); + Rect bounds = getBounds(); + mPath.addOval(bounds.left, bounds.top, bounds.right, bounds.bottom, Path.Direction.CW); + return mPath; + } + + @Override + public void draw(Canvas canvas) { + int save = canvas.save(); + canvas.clipPath(getIconMask()); + + Drawable d; + if ((d = getBackground()) != null) { + d.draw(canvas); + } + if ((d = getForeground()) != null) { + d.draw(canvas); + } + canvas.restoreToCount(save); + } + } +} diff --git a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java index 8442eed..6724d6b 100644 --- a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java +++ b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.graphics.BlendMode; import android.graphics.BlendModeColorFilter; import android.graphics.Canvas; +import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Rect; @@ -128,13 +129,8 @@ public class ThemedIconDrawable extends FastBitmapDrawable { public static int[] getColors(Context context) { Resources res = context.getResources(); int[] colors = new int[2]; - if ((res.getConfiguration().uiMode & UI_MODE_NIGHT_MASK) == UI_MODE_NIGHT_YES) { - colors[0] = res.getColor(android.R.color.system_neutral1_800); - colors[1] = res.getColor(android.R.color.system_accent1_100); - } else { - colors[0] = res.getColor(android.R.color.system_accent1_100); - colors[1] = res.getColor(android.R.color.system_neutral2_700); - } + colors[0] = res.getColor(R.color.themed_icon_background_color); + colors[1] = res.getColor(R.color.themed_icon_color); return colors; } diff --git a/motiontoollib/build.gradle b/motiontoollib/build.gradle index a1f39b9..e3750ec 100644 --- a/motiontoollib/build.gradle +++ b/motiontoollib/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'sysuigradleproject.android-library-conventions' + id 'com.android.library' id 'org.jetbrains.kotlin.android' id 'com.google.protobuf' } @@ -7,18 +7,12 @@ plugins { final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/motiontoollib/src/com/android/app/motiontool/proto" android { - compileSdk TARGET_SDK.toInteger() - buildToolsVersion = BUILD_TOOLS_VERSION - + namespace = "com.android.app.motiontool" + testNamespace = "com.android.app.motiontool.tests" defaultConfig { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - defaultConfig { - minSdkVersion TARGET_SDK.toInteger() - targetSdkVersion TARGET_SDK.toInteger() - } - sourceSets { main { java.srcDirs = ['src'] @@ -30,15 +24,15 @@ android { manifest.srcFile "tests/AndroidManifest.xml" } } - - lintOptions { + lint { abortOnError false } + } dependencies { implementation "androidx.core:core:1.9.0" - implementation "com.google.protobuf:protobuf-lite:${protobuf_version}" + implementation "com.google.protobuf:protobuf-lite:${protobuf_lite_version}" api project(":ViewCaptureLib") androidTestImplementation project(':SharedTestLib') androidTestImplementation 'androidx.test.ext:junit:1.1.3' diff --git a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt index 63a6fee..c7a6b0d 100644 --- a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt +++ b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt @@ -107,7 +107,7 @@ class DdmHandleMotionTool private constructor( MotionToolsResponse.newBuilder().apply { tryCatchingMotionToolManagerExceptions { setPollTrace(PollTraceResponse.newBuilder() - .setExportedData(motionToolManager.pollTrace(pollTraceRequest.traceId))) + .setData(motionToolManager.pollTrace(pollTraceRequest.traceId))) } }.build() @@ -115,7 +115,7 @@ class DdmHandleMotionTool private constructor( MotionToolsResponse.newBuilder().apply { tryCatchingMotionToolManagerExceptions { setEndTrace(EndTraceResponse.newBuilder() - .setExportedData(motionToolManager.endTrace(endTraceRequest.traceId))) + .setData(motionToolManager.endTrace(endTraceRequest.traceId))) } }.build() diff --git a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt index 66b00f7..a98a588 100644 --- a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt +++ b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt @@ -22,8 +22,9 @@ import android.view.Choreographer import android.view.View import android.view.WindowManagerGlobal import androidx.annotation.VisibleForTesting +import com.android.app.viewcapture.SimpleViewCapture import com.android.app.viewcapture.ViewCapture -import com.android.app.viewcapture.data.ExportedData +import com.android.app.viewcapture.data.MotionWindowData /** * Singleton to manage motion tracing sessions. @@ -43,7 +44,7 @@ import com.android.app.viewcapture.data.ExportedData * @see [DdmHandleMotionTool] */ class MotionToolManager private constructor(private val windowManagerGlobal: WindowManagerGlobal) { - private val viewCapture: ViewCapture = SimpleViewCapture() + private val viewCapture: ViewCapture = SimpleViewCapture("MTViewCapture") companion object { private const val TAG = "MotionToolManager" @@ -77,29 +78,29 @@ class MotionToolManager private constructor(private val windowManagerGlobal: Win } /** - * Ends [ViewCapture] and returns the captured [ExportedData] since the [beginTrace] call or the - * last [pollTrace] call. + * Ends [ViewCapture] and returns the captured [MotionWindowData] since the [beginTrace] call or + * the last [pollTrace] call. */ @Synchronized - fun endTrace(traceId: Int): ExportedData { + fun endTrace(traceId: Int): MotionWindowData { Log.d(TAG, "End Trace for id: $traceId") val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) } - val exportedData = pollTrace(traceId) + val data = pollTrace(traceId) traceMetadata.stopTrace() traces.remove(traceId) - return exportedData + return data } /** - * Returns the [ExportedData] captured since the [beginTrace] call or the last [pollTrace] call. + * Returns the [MotionWindowData] captured since the [beginTrace] call or last [pollTrace] call. * This function can only be used after [beginTrace] is called and before [endTrace] is called. */ @Synchronized - fun pollTrace(traceId: Int): ExportedData { + fun pollTrace(traceId: Int): MotionWindowData { val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) } - val exportedData = getExportedDataFromViewCapture(traceMetadata) - traceMetadata.updateLastPolledTime(exportedData) - return exportedData + val data = getDataFromViewCapture(traceMetadata) + traceMetadata.updateLastPolledTime(data) + return data } /** @@ -115,32 +116,26 @@ class MotionToolManager private constructor(private val windowManagerGlobal: Win traceIdCounter = 0 } - private fun getExportedDataFromViewCapture(traceMetadata: TraceMetadata): ExportedData { + private fun getDataFromViewCapture(traceMetadata: TraceMetadata): MotionWindowData { val rootView = getRootView(traceMetadata.windowId) ?: throw WindowNotFoundException(traceMetadata.windowId) - val exportedData = viewCapture - .getDumpTask(rootView) - ?.orElse(null) - ?.get() ?: return ExportedData.newBuilder().build() - - val filteredFrameData = exportedData.frameDataList - ?.filter { it.timestamp > traceMetadata.lastPolledTime } - - return exportedData.toBuilder() - .clearFrameData() - .addAllFrameData(filteredFrameData) - .build() + val data: MotionWindowData = viewCapture + .getDumpTask(rootView).get() + ?.orElse(null) ?: return MotionWindowData.newBuilder().build() + val filteredFrameData = data.frameDataList.filter { + it.timestamp > traceMetadata.lastPolledTime + } + return data.toBuilder() + .clearFrameData() + .addAllFrameData(filteredFrameData) + .build() } private fun getRootView(windowId: String): View? { return windowManagerGlobal.getRootView(windowId) } - - class SimpleViewCapture : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, - MAIN_EXECUTOR.submit { Choreographer.getInstance() }.get(), - createAndStartNewLooperExecutor("MTViewCapture", Process.THREAD_PRIORITY_FOREGROUND)) } private data class TraceMetadata( @@ -148,9 +143,9 @@ private data class TraceMetadata( var lastPolledTime: Long, var stopTrace: () -> Unit ) { - fun updateLastPolledTime(exportedData: ExportedData?) { - exportedData?.frameDataList?.maxOfOrNull { it.timestamp }?.let { maxFrameTimestamp -> - lastPolledTime = maxFrameTimestamp + fun updateLastPolledTime(data: MotionWindowData?) { + data?.frameDataList?.maxOfOrNull { it.timestamp }?.let { + lastPolledTime = it } } } diff --git a/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto index cd5ad2f..04ee020 100644 --- a/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto +++ b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto @@ -98,7 +98,7 @@ message EndTraceRequest { } message EndTraceResponse { - optional com.android.app.viewcapture.data.ExportedData exported_data = 1; + optional com.android.app.viewcapture.data.MotionWindowData data = 1; } // Polls collected motion trace data collected since the last PollTraceRequest (or the @@ -108,6 +108,6 @@ message PollTraceRequest { } message PollTraceResponse { - optional com.android.app.viewcapture.data.ExportedData exported_data = 1; + optional com.android.app.viewcapture.data.MotionWindowData data = 1; } diff --git a/motiontoollib/tests/AndroidManifest.xml b/motiontoollib/tests/AndroidManifest.xml index 3db8d2f..c16e25f 100644 --- a/motiontoollib/tests/AndroidManifest.xml +++ b/motiontoollib/tests/AndroidManifest.xml @@ -15,14 +15,14 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.app.motiontool"> + package="com.android.app.motiontool.tests"> <application android:debuggable="true" android:theme="@android:style/Theme.NoTitleBar"> <activity - android:name=".util.TestActivity" + android:name="com.android.app.motiontool.util.TestActivity" android:exported="false" /> <uses-library android:name="android.test.runner" /> @@ -32,6 +32,6 @@ <instrumentation android:name="android.testing.TestableInstrumentation" android:label="Tests for MotionTool Lib" - android:targetPackage="com.android.app.motiontool"/> + android:targetPackage="com.android.app.motiontool.tests"/> </manifest> diff --git a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt index 7d78237..f112d0b 100644 --- a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt +++ b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt @@ -119,7 +119,7 @@ class DdmHandleMotionToolTest { activityScenarioRule.scenario.onActivity { val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId()) val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId) - Assert.assertTrue(endTraceResponse.endTrace.exportedData.frameDataList.isEmpty()) + Assert.assertTrue(endTraceResponse.endTrace.data.frameDataList.isEmpty()) } } @@ -130,14 +130,17 @@ class DdmHandleMotionToolTest { val traceId = beginTraceResponse.beginTrace.traceId Choreographer.getInstance().postFrameCallback { - activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw() + activity + .requireViewById<View>(android.R.id.content) + .viewTreeObserver + .dispatchOnDraw() val pollTraceResponse = performPollTraceRequest(traceId) - assertEquals(1, pollTraceResponse.pollTrace.exportedData.frameDataList.size) + assertEquals(1, pollTraceResponse.pollTrace.data.frameDataList.size) // Verify that frameData is only included once and is not returned again val endTraceResponse = performEndTraceRequest(traceId) - assertEquals(0, endTraceResponse.endTrace.exportedData.frameDataList.size) + assertEquals(0, endTraceResponse.endTrace.data.frameDataList.size) } } } diff --git a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt index 560f798..05400a2 100644 --- a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt +++ b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt @@ -85,14 +85,17 @@ class MotionToolManagerTest { activityScenarioRule.scenario.onActivity { activity -> val traceId = motionToolManager.beginTrace(getActivityViewRootId()) Choreographer.getInstance().postFrameCallback { - activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw() + activity + .requireViewById<View>(android.R.id.content) + .viewTreeObserver + .dispatchOnDraw() - val polledExportedData = motionToolManager.pollTrace(traceId) - assertEquals(1, polledExportedData.frameDataList.size) + val polledData = motionToolManager.pollTrace(traceId) + assertEquals(1, polledData.frameDataList.size) // Verify that frameData is only included once and is not returned again - val endExportedData = motionToolManager.endTrace(traceId) - assertEquals(0, endExportedData.frameDataList.size) + val endData = motionToolManager.endTrace(traceId) + assertEquals(0, endData.frameDataList.size) } } } diff --git a/searchuilib/.gitignore b/searchuilib/.gitignore deleted file mode 100644 index 6213826..0000000 --- a/searchuilib/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -*.iml -.project -.classpath -.project.properties -gen/ -bin/ -.idea/ -.gradle/ -local.properties -gradle/ -build/ -gradlew* -.DS_Store diff --git a/searchuilib/Android.bp b/searchuilib/Android.bp deleted file mode 100644 index f7b0b83..0000000 --- a/searchuilib/Android.bp +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (C) 2020 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 { - default_applicable_licenses: ["Android-Apache-2.0"], -} - -android_library { - name: "search_ui", - - sdk_version: "system_current", - - static_libs: [ - "androidx.annotation_annotation", - ], - srcs: [ - "src/**/*.java", - ], -} diff --git a/searchuilib/build.gradle b/searchuilib/build.gradle deleted file mode 100644 index 6b5027b..0000000 --- a/searchuilib/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - id 'com.android.library' -} - -android { - compileSdk TARGET_SDK.toInteger() - buildToolsVersion = BUILD_TOOLS_VERSION - - defaultConfig { - minSdkVersion TARGET_SDK.toInteger() - targetSdkVersion TARGET_SDK.toInteger() - } - - sourceSets { - main { - java.srcDirs = ['src'] - manifest.srcFile 'AndroidManifest.xml' - } - } - - lintOptions { - abortOnError false - } - - tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} - -dependencies { - implementation "androidx.core:core:+" -} diff --git a/searchuilib/src/com/android/app/search/LayoutType.java b/searchuilib/src/com/android/app/search/LayoutType.java deleted file mode 100644 index 53c663d..0000000 --- a/searchuilib/src/com/android/app/search/LayoutType.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (C) 2020 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.app.search; - -/** - * Constants to be used with {@link SearchTarget}. - */ -public class LayoutType { - - // ------ - // | icon | - // ------ - // text - public static final String ICON_SINGLE_VERTICAL_TEXT = "icon"; - - // Below three layouts (to be deprecated) and two layouts render - // {@link SearchTarget}s in following layout. - // ------ ------ ------ - // | | title |(opt)| |(opt)| - // | icon | subtitle (optional) | icon| | icon| - // ------ ------ ------ - @Deprecated - public static final String ICON_SINGLE_HORIZONTAL_TEXT = "icon_text_row"; - @Deprecated - public static final String ICON_DOUBLE_HORIZONTAL_TEXT = "icon_texts_row"; - @Deprecated - public static final String ICON_DOUBLE_HORIZONTAL_TEXT_BUTTON = "icon_texts_button"; - - // will replace ICON_DOUBLE_* ICON_SINGLE_* layouts - public static final String ICON_HORIZONTAL_TEXT = "icon_row"; - public static final String HORIZONTAL_MEDIUM_TEXT = "icon_row_medium"; - public static final String EXTRA_TALL_ICON_ROW = "extra_tall_icon_row"; - public static final String SMALL_ICON_HORIZONTAL_TEXT = "short_icon_row"; - public static final String SMALL_ICON_HORIZONTAL_TEXT_THUMBNAIL = "short_icon_row_thumbnail"; - - // This layout contains a series of icon results (currently up to 4 per row). - // The container does not support stretching for its children, and can only contain - // {@link #ICON_SINGLE_VERTICAL_TEXT} layout types. - public static final String ICON_CONTAINER = "icon_container"; - - // This layout contains a series of thumbnails (currently up to 3 per row). - // The container supports stretching for its children, and can only contain {@link #THUMBNAIL} - // layout types. - public static final String THUMBNAIL_CONTAINER = "thumbnail_container"; - - // This layout creates a container for people grouping - // Only available above version code 2 - public static final String BIG_ICON_MEDIUM_HEIGHT_ROW = "big_icon_medium_row"; - - // This layout creates square thumbnail image (currently 3 column) - public static final String THUMBNAIL = "thumbnail"; - - // This layout contains an icon and slice - public static final String ICON_SLICE = "slice"; - - // Widget bitmap preview - public static final String WIDGET_PREVIEW = "widget_preview"; - - // Live widget search result - public static final String WIDGET_LIVE = "widget_live"; - - // Layout type used to display people tiles using shortcut info - public static final String PEOPLE_TILE = "people_tile"; - - // Deprecated - // text based header to group various layouts in low confidence section of the results. - public static final String TEXT_HEADER = "header"; - - // horizontal bar to be inserted between fallback search results and low confidence section - public static final String EMPTY_DIVIDER = "empty_divider"; - - // layout representing quick calculations - public static final String CALCULATOR = "calculator"; - - // From version code 4, if TEXT_HEADER_ROW is used, no need to insert this on-device - // section header. - public static final String SECTION_HEADER = "section_header"; - - // layout for a tall card with header and image, and no icon. - public static final String TALL_CARD_WITH_IMAGE_NO_ICON = "tall_card_with_image_no_icon"; - - // Layout for a text header - // Available for SearchUiManager proxy service to use above version code 3 - public static final String TEXT_HEADER_ROW = "text_header_row"; - - // Layout for a quick settings tile - public static final String QS_TILE = "qs_tile"; - - // Placeholder for web suggest. - public static final String PLACEHOLDER = "placeholder"; - - // Placeholder for rich answer cards. - // Only available on or above version code 3. - public static final String RICHANSWER_PLACEHOLDER = "richanswer_placeholder"; - -} diff --git a/searchuilib/src/com/android/app/search/QueryExtras.java b/searchuilib/src/com/android/app/search/QueryExtras.java deleted file mode 100644 index a6dbba1..0000000 --- a/searchuilib/src/com/android/app/search/QueryExtras.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * 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 com.android.app.search; - -import android.app.search.Query; - -/** - * Utility class used to define implicit contract between aiai and launcher regarding - * what constant string key should be used to pass sub session information inside - * the {@link Query} object. - * - * This decorated query object is passed to aiai using two method calls: - * <ul> - * <ol>android.app.search.SearchSession.query()</ol> - * <ol>android.app.search.SearchSession.notifyEvent()</ol> - * </ul> - */ -public class QueryExtras { - - // Can be either 1 (ALLAPPS) or 2 (QSB) - public static final String EXTRAS_KEY_ENTRY = "entry"; - - // This value overrides the timeout that is defined inside {@link SearchContext#getTimeout} - public static final String EXTRAS_KEY_TIMEOUT_OVERRIDE = "timeout"; - - // Used to know which target is deleted. - public static final String EXTRAS_BUNDLE_DELETED_TARGET_ID = "deleted_target_id"; -} diff --git a/searchuilib/src/com/android/app/search/ResultType.java b/searchuilib/src/com/android/app/search/ResultType.java deleted file mode 100644 index 9dd9dad..0000000 --- a/searchuilib/src/com/android/app/search/ResultType.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright (C) 2020 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.app.search; - -/** - * Constants to be used with {@link android.app.search.SearchContext} and - * {@link android.app.search.SearchTarget}. - * - * Note, a result type could be a of two types. - * For example, unpublished settings result type could be in slices: - * <code> resultType = SETTING | SLICE </code> - */ -public class ResultType { - - // published corpus by 3rd party app, supported by SystemService - public static final int APPLICATION = 1 << 0; - public static final int SHORTCUT = 1 << 1; - public static final int SLICE = 1 << 6; - public static final int WIDGETS = 1 << 7; - - // Not extracted from any of the SystemService - public static final int PEOPLE = 1 << 2; - public static final int ACTION = 1 << 3; - public static final int SETTING = 1 << 4; - public static final int IMAGE = 1 << 5; - public static final int PLAY = 1 << 8; - public static final int SUGGEST = 1 << 9; - public static final int ASSISTANT = 1 << 10; - public static final int CHROMETAB = 1 << 11; - public static final int NAVVYSITE = 1 << 12; - public static final int TIPS = 1 << 13; - public static final int PEOPLE_TILE = 1 << 14; - public static final int LEGACY_SHORTCUT = 1 << 15; - public static final int MEMORY = 1 << 16; - public static final int WEB_SUGGEST = 1 << 17; - public static final int NO_FULFILLMENT = 1 << 18; - public static final int EDUCARD = 1 << 19; - public static final int SYSTEM_POINTER = 1 << 20; - public static final int VIDEO = 1 << 21; - - public static final int PUBLIC_DATA_TYPE = APPLICATION | SETTING | PLAY | WEB_SUGGEST; - public static final int PRIMITIVE_TYPE = APPLICATION | SLICE | SHORTCUT | WIDGETS | ACTION | - LEGACY_SHORTCUT; - public static final int CORPUS_TYPE = - PEOPLE | SETTING | IMAGE | PLAY | SUGGEST | ASSISTANT | CHROMETAB | NAVVYSITE | TIPS - | PEOPLE_TILE | MEMORY | WEB_SUGGEST | VIDEO; - public static final int RANK_TYPE = SYSTEM_POINTER; - public static final int UI_TYPE = EDUCARD | NO_FULFILLMENT; - - public static boolean isSlice(int resultType) { - return (resultType & SLICE) != 0; - } - - public static boolean isSystemPointer(int resultType) { - return (resultType & SYSTEM_POINTER) != 0; - } - - /** - * Returns result type integer where only {@code #CORPUS_TYPE} bit will turned on. - */ - public static int getCorpusType(int resultType) { - return (resultType & CORPUS_TYPE); - } - - /** - * Returns result type integer where only {@code #PRIMITIVE_TYPE} bit will be turned on. - */ - public static int getPrimitiveType(int resultType) { - return (resultType & PRIMITIVE_TYPE); - } - - /** - * Returns whether the given result type is privacy safe or not. - */ - public static boolean isPrivacySafe(int resultType) { - return (resultType & PUBLIC_DATA_TYPE) != 0; - } -} diff --git a/searchuilib/src/com/android/app/search/SearchActionExtras.java b/searchuilib/src/com/android/app/search/SearchActionExtras.java deleted file mode 100644 index 2f33d5d..0000000 --- a/searchuilib/src/com/android/app/search/SearchActionExtras.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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 com.android.app.search; - -import android.app.search.SearchAction; - -/** - * Helper class that defines key string value for {@link SearchAction#getExtras()} - */ -public class SearchActionExtras { - public static final String BUNDLE_EXTRA_HIDE_SUBTITLE = "hide_subtitle"; - public static final String BUNDLE_EXTRA_HIDE_ICON = "hide_icon"; - public static final String BUNDLE_EXTRA_ALLOW_PINNING = "allow_pinning"; - public static final String BUNDLE_EXTRA_BADGE_WITH_PACKAGE = "badge_with_package"; - public static final String BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE = "primary_icon_from_title"; - public static final String BUNDLE_EXTRA_IS_SEARCH_IN_APP = "is_search_in_app"; - public static final String BUNDLE_EXTRA_BADGE_WITH_COMPONENT_NAME = "badge_with_component_name"; - public static final String BUNDLE_EXTRA_ICON_CACHE_KEY = "icon_cache_key"; - public static final String BUNDLE_EXTRA_ICON_TOKEN_INTEGER = "icon_integer"; - public static final String BUNDLE_EXTRA_SHOULD_START = "should_start"; - public static final String BUNDLE_EXTRA_SHOULD_START_FOR_RESULT = "should_start_for_result"; - public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_TEXT = "suggestion_action_text"; - public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_RPC = "suggestion_action_rpc"; - public static final String BUNDLE_EXTRA_SKIP_LOGGING_IN_TARGET_HANDLER = - "skip_logging_in_target_handler"; -} diff --git a/searchuilib/src/com/android/app/search/SearchTargetConverter.java b/searchuilib/src/com/android/app/search/SearchTargetConverter.java deleted file mode 100644 index 2080966..0000000 --- a/searchuilib/src/com/android/app/search/SearchTargetConverter.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 com.android.app.search; - -import static com.android.app.search.LayoutType.SMALL_ICON_HORIZONTAL_TEXT; -import static com.android.app.search.SearchActionExtras.BUNDLE_EXTRA_HIDE_ICON; -import static com.android.app.search.SearchActionExtras.BUNDLE_EXTRA_HIDE_SUBTITLE; -import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_CLASS; -import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_SUBTITLE_OVERRIDE; -import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER; -import static com.android.app.search.SearchTargetExtras.EXTRAS_RECENT_BLOCK_TARGET; - -import android.app.search.SearchAction; -import android.app.search.SearchTarget; -import android.content.pm.ShortcutInfo; -import android.os.Bundle; - -public class SearchTargetConverter { - /** - * Generate a searchTarget that uses {@link LayoutType#SMALL_ICON_HORIZONTAL_TEXT} from a - * searchTarget where original layout type may not have been SMALL_ICON_HORIZONTAL_TEXT. Only - * possible if the given SearchTarget contains a searchAction or shortcutInfo, otherwise the - * original searchTarget will be returned. - */ - public static SearchTarget convertLayoutTypeToSmallIconHorizontalText( - SearchTarget searchTarget) { - SearchAction searchTargetAction = searchTarget.getSearchAction(); - ShortcutInfo shortcutInfo = searchTarget.getShortcutInfo(); - int resultType = searchTarget.getResultType(); - String subtitle = ""; - - Bundle searchTargetBundle = searchTarget.getExtras(); - searchTargetBundle.putString(BUNDLE_EXTRA_CLASS, - searchTargetBundle.getString(BUNDLE_EXTRA_CLASS)); - searchTargetBundle.putBoolean(BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER, true); - searchTargetBundle.putBoolean(BUNDLE_EXTRA_HIDE_SUBTITLE, false); - searchTargetBundle.putString(BUNDLE_EXTRA_SUBTITLE_OVERRIDE, subtitle); - searchTargetBundle.putBoolean(BUNDLE_EXTRA_HIDE_ICON, false); - searchTargetBundle.putBoolean(EXTRAS_RECENT_BLOCK_TARGET, true); - - SearchTarget.Builder builder = new SearchTarget.Builder(resultType, - SMALL_ICON_HORIZONTAL_TEXT, searchTarget.getId()) - .setPackageName(searchTarget.getPackageName()) - .setExtras(searchTargetBundle) - .setUserHandle(searchTarget.getUserHandle()); - if (searchTargetAction != null) { - builder.setSearchAction(searchTargetAction); - } else if (shortcutInfo != null) { - builder.setShortcutInfo(shortcutInfo); - } else { - return searchTarget; - } - return builder.build(); - } -} diff --git a/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java b/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java deleted file mode 100644 index a323625..0000000 --- a/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * 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 com.android.app.search; - -import static com.android.app.search.SearchTargetExtras.isRichAnswer; - -import android.app.search.SearchTarget; -import android.content.ComponentName; -import android.os.Process; - -import androidx.annotation.Nullable; - -/** - * Helper class that defines helper methods for {@link android.app.search.SearchTargetEvent} to - * define the contract between Launcher and AiAi for notifyEvent. - */ - -public class SearchTargetEventHelper { - - public static final String PKG_NAME_AGSA = "com.google.android.googlequicksearchbox"; - - /** - * Generate web target id similar to AiAi targetId for logging search button tap and Launcher - * sends raw query to AGA. - * AiAi target id is of format "resultType:userId:packageName:extraInfo" - * - * @return string webTargetId - * Example webTargetId for - * web suggestion - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:SUGGESTION - */ - public static String generateWebTargetIdForRawQuery() { - // For raw query, there is no search target, so we pass null. - return generateWebTargetIdForLogging(null); - } - - /** - * Generate web target id similar to AiAi targetId for logging both 0-state and n-state. - * AiAi target id is of format "resultType:userId:packageName:extraInfo" - * - * @return string webTargetId - * Example webTargetId for - * web suggestion - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:SUGGESTION - * rich answer - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:RICH_ANSWER - */ - public static String generateWebTargetIdForLogging(@Nullable SearchTarget webTarget) { - StringBuilder webTargetId = new StringBuilder( - "WEB_SUGGEST" + ":" + Process.myUserHandle().getIdentifier() + ":"); - if (webTarget == null) { - webTargetId.append(PKG_NAME_AGSA + ":SUGGESTION"); - return webTargetId.toString(); - } - webTargetId.append(webTarget.getPackageName()); - if (isRichAnswer(webTarget)) { - webTargetId.append(":RICH_ANSWER"); - } else { - webTargetId.append(":SUGGESTION"); - } - return webTargetId.toString(); - } - - /** - * Generate application target id similar to AiAi targetId for logging only 0-state. - * For n-state, AiAi already populates the target id in right format. - * AiAi target id is of format "resultType:userId:packageName:extraInfo" - * - * When the apps from AiAi's AppPredictionService are converted to {@link SearchTarget}, we need - * to construct the targetId using componentName. - * - * @return string appTargetId - * Example appTargetId for - * maps - APPLICATION:0:com.google.android.apps.maps:com.google.android.maps.MapsActivity - * clock - APPLICATION:0:com.google.android.deskclock:com.android.deskclock.DeskClock - */ - public static String generateAppTargetIdForLogging(@Nullable ComponentName appComponentName) { - StringBuilder appTargetId = new StringBuilder( - "APPLICATION" + ":" + Process.myUserHandle().getIdentifier() + ":"); - if (appComponentName == null) return appTargetId.append(" : ").toString(); - return appTargetId + appComponentName.getPackageName() + ":" - + appComponentName.getClassName(); - } - - /** - * Generate gms play target id similar to AiAi targetId for logging only n-state. - * AiAi target id is of format "resultType:userId:packageName:extraInfo" - * - * @return string playTargetId - * Example playTargetId for Candy Crush - * PLAY:0:com.king.candycrushsaga:Gms - */ - public static String generatePlayTargetIdForLogging(String appPackage) { - return "PLAY" + ":" + Process.myUserHandle().getIdentifier() + ":" + appPackage + ":Gms"; - } -} diff --git a/searchuilib/src/com/android/app/search/SearchTargetExtras.java b/searchuilib/src/com/android/app/search/SearchTargetExtras.java deleted file mode 100644 index 887c457..0000000 --- a/searchuilib/src/com/android/app/search/SearchTargetExtras.java +++ /dev/null @@ -1,228 +0,0 @@ -/* - * 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 com.android.app.search; - -import static com.android.app.search.LayoutType.TALL_CARD_WITH_IMAGE_NO_ICON; - -import android.app.blob.BlobHandle; -import android.app.search.SearchAction; -import android.app.search.SearchTarget; -import android.os.Bundle; -import android.text.TextUtils; - -import androidx.annotation.Nullable; - -/** - * Helper class that defines key string value for {@link SearchTarget#getExtras()} - * and also defines helper methods - */ -public class SearchTargetExtras { - - /** on device data related extras and helper methods */ - // Used to extra component name. - public static final String BUNDLE_EXTRA_CLASS = "class"; - - // Used for UI treatment. Labels whether search target should support quick launch. - public static final String BUNDLE_EXTRA_QUICK_LAUNCH = "quick_launch"; - // Used for UI treatment. Targets grouped with same group id are decorated together. - public static final String BUNDLE_EXTRA_GROUP_ID = "group_id"; - public static final String BUNDLE_EXTRA_GROUP_DECORATE_TOGETHER = "decorate_together"; - // Used if slice title should be rendered else where outside of slice (e.g., edit text). - public static final String BUNDLE_EXTRA_SLICE_TITLE = "slice_title"; - // Used if slice view should be rendered using full height mode. - public static final String BUNDLE_EXTRA_USE_FULL_HEIGHT = "use_full_height"; - public static final String BUNDLE_EXTRA_IS_NON_TAPPABLE = "is_non_tappable"; - public static final String BUNDLE_EXTRA_TITLE_OVERWRITE = "title_overwrite"; - // Used if subtitle view should be overridden to string that is not natively defined by the - // search target. - public static final String BUNDLE_EXTRA_SUBTITLE_OVERRIDE = "subtitle_override"; - - // Used for logging. Returns whether spelling correction was applied. - public static final String BUNDLE_EXTRA_IS_QUERY_CORRECTED = "is_query_corrected"; - // Used for logging. Returns whether the result matched block title or the inline item. - public static final String BUNDLE_EXTRA_RESULT_MATCH_USER_TYPED = "result_match_user_typed"; - // Used for logging. Returns the timestamp when system service received the data. - public static final String BUNDLE_EXTRA_START_TIMESTAMP = "start_timestamp"; - // Indicates the search result app location column. - public static final String BUNDLE_EXTRA_RESULT_APP_GRIDX = "app_gridx"; - - // Used for thumbnail loading. Contains handle to retrieve Blobstore asset. - public static final String BUNDLE_EXTRA_BLOBSTORE_HANDLE = "blobstore_handle_key"; - - // Used to denote this searchTarget is for recent block in 0-state. - public static final String EXTRAS_RECENT_BLOCK_TARGET = "recent_block_target"; - - public static final int GROUPING = 1 << 1; - - @Nullable - public static String getDecoratorId(@Nullable SearchTarget target) { - return isTargetOrExtrasNull(target) ? null : - target.getExtras().getString(BUNDLE_EXTRA_GROUP_ID); - } - - public static int getDecoratorType(@Nullable SearchTarget target) { - int type = 0; - if (isTargetOrExtrasNull(target)) { - return type; - } - if (!TextUtils.isEmpty(target.getExtras().getString(BUNDLE_EXTRA_GROUP_ID))) { - type |= GROUPING; - } - return type; - } - - /** Whether or not the SearchTarget's Extras contains a blobstore image. */ - public static boolean isSearchTargetBlobstoreAsset(@Nullable SearchTarget target) { - if (isTargetOrExtrasNull(target)) { - return false; - } - return target.getExtras().getParcelable( - BUNDLE_EXTRA_BLOBSTORE_HANDLE) instanceof BlobHandle; - } - - /** Check if SearchTarget contains information to tell if this target is from recent block. */ - public static boolean isSearchTargetRecentItem(@Nullable SearchTarget target) { - if (isTargetOrExtrasNull(target)) { - return false; - } - return target.getExtras().getBoolean(EXTRAS_RECENT_BLOCK_TARGET, false); - } - - private static boolean isTargetOrExtrasNull(@Nullable SearchTarget target) { - return target == null || target.getExtras() == null; - } - - /** Web data related extras and helper methods */ - public static final String BUNDLE_EXTRA_PROXY_WEB_ITEM = "proxy_web_item"; - public static final String BUNDLE_EXTRA_ENTITY = "is_entity"; - public static final String BUNDLE_EXTRA_ANSWER = "is_answer"; - public static final String BUNDLE_EXTRA_RESPONSE_ID = "response_id"; - public static final String BUNDLE_EXTRA_LEARN_MORE_URL = "learn_more_url"; - public static final String BUNDLE_EXTRA_PERSONAL = "is_personal"; - public static final String BUNDLE_EXTRA_SUGGESTION_TYPE = "suggestion_type"; - public static final String BUNDLE_EXTRA_SUGGEST_RENDER_TEXT = "suggest_render_text"; - public static final String BUNDLE_EXTRA_ZERO_STATE_CACHE = "zero_state_cache"; - public static final String BUNDLE_EXTRA_TALL_CARD_HEADER = "tall_card_header"; - public static final String BUNDLE_EXTRA_TALL_CARD_IMAGE_DESCRIPTION = - "tall_card_image_description"; - public static final String BUNDLE_EXTRA_BITMAP_URL = "bitmap_url"; - - // Used for web suggestions count for both AA+ and QSB entry point. - // Returns the number of web suggestions to be shown. - public static final String WEB_SUG_COUNT = "web_sug_count"; - - /** - * Replaced with thumbnail crop type - * - * Flag to control whether thumbnail(s) should fill the thumbnail container's width or not. - * When this flag is true, when there are less than the maximum number of thumbnails in the - * container, the thumbnails will stretch to fill the container's width. - * When this flag is false, thumbnails will always be cropped to a square ratio even if - * there aren't enough thumbnails to fill the container. - * - * Only relevant in {@link LayoutType#THUMBNAIL_CONTAINER} and {@link LayoutType#THUMBNAIL}. - */ - @Deprecated - public static final String BUNDLE_EXTRA_SHOULD_FILL_CONTAINER_WIDTH = - "should_fill_container_width"; - - /** - * Flag to control thumbnail container's crop mode, controlling the layout - * - * <ul> - * <li>SQUARE: Thumbnail(s) will be cropped to a square aspect ratio around the center.</li> - * <li>FILL_WIDTH: Thumbnail(s) should collectively fill the thumbnail container's width. - * When there are less than the maximum number of thumbnails in the container, the - * layouts' width will stretch to fit the container, the images will fill the width - * and then the top/bottom cropped to fit.</li> - * <li>FILL_HEIGHT: Thumbnail(s) should fill height and be cropped to fit in the width - * based on {@link BUNDLE_EXTRA_THUMBNAIL_MAX_COUNT} as the column count. When the image - * width is larger than the width / column, both sides will be cropped while maintaining - * the center. - * When there are less thumbnails than the max count, the layout will be constrained to - * equally divide the width of the container. If there are more thumbnails than the max - * count, the excessive thumbnails will be ignored.</li> - * </ul> - * - * Only relevant in {@link LayoutType#THUMBNAIL_CONTAINER} and {@link LayoutType#THUMBNAIL}. - */ - public static final String BUNDLE_EXTRA_THUMBNAIL_CROP_TYPE = "thumbnail_crop_type"; - public enum ThumbnailCropType { - DEFAULT(0), // defaults to SQUARE behavior by {@link LayoutType#THUMBNAIL_CONTAINER}. - SQUARE(1), - FILL_WIDTH(2), - FILL_HEIGHT(3); - - private final int mTypeId; - - ThumbnailCropType(int typeId) { - mTypeId = typeId; - } - - public int toTypeId() { - return mTypeId; - } - }; - - /** - * How many grid spaces for the thumbnail container should be reserved. - * Only relevant for {@link ThumbnailCropType#FILL_HEIGHT} crop type. - */ - public static final String BUNDLE_EXTRA_THUMBNAIL_MAX_COUNT = "thumbnail_max_count"; - - /** - * Flag to control whether the SearchTarget's label should be hidden. - * When this flag is true, label will be hidden. - * When this flag is false (or omitted), {@link SearchAction#mTitle} will be shown. - */ - public static final String BUNDLE_EXTRA_HIDE_LABEL = - "hide_label"; - public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_TEXT = "suggestion_action_text"; - public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_RPC = "suggestion_action_rpc"; - public static final String BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER = "support_query_builder"; - public static final String BUNDLE_EXTRA_SUGGEST_RAW_TEXT = "suggest_raw_text"; - public static final String BUNDLE_EXTRA_SUGGEST_TRUNCATE_START = "suggest_truncate_start"; - - /** Web data related helper methods */ - public static boolean isEntity(@Nullable SearchTarget target) { - return target != null && target.getExtras() != null - && target.getExtras().getBoolean(BUNDLE_EXTRA_ENTITY); - } - - public static boolean isAnswer(@Nullable SearchTarget target) { - return target != null && target.getExtras() != null - && target.getExtras().getBoolean(BUNDLE_EXTRA_ANSWER); - } - - /** Whether the search target is a rich answer web result. */ - public static boolean isRichAnswer(@Nullable SearchTarget target) { - return target != null && isAnswer(target) - && target.getLayoutType().equals(TALL_CARD_WITH_IMAGE_NO_ICON); - } - - /** Get the crop type thumbnails should use. Returns DEFAULT if not specified. */ - public static ThumbnailCropType getThumbnailCropType(@Nullable SearchTarget target) - throws ArrayIndexOutOfBoundsException { - Bundle extras = target == null ? Bundle.EMPTY : target.getExtras(); - if (extras.isEmpty()) { - return ThumbnailCropType.DEFAULT; - } - ThumbnailCropType cropType = ThumbnailCropType.values()[extras.getInt( - BUNDLE_EXTRA_THUMBNAIL_CROP_TYPE)]; - return cropType != null ? cropType : ThumbnailCropType.DEFAULT; - } -} diff --git a/searchuilib/src/com/android/app/search/SearchTargetGenerator.java b/searchuilib/src/com/android/app/search/SearchTargetGenerator.java deleted file mode 100644 index 22e5a86..0000000 --- a/searchuilib/src/com/android/app/search/SearchTargetGenerator.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 com.android.app.search; - -import static com.android.app.search.LayoutType.EMPTY_DIVIDER; -import static com.android.app.search.LayoutType.SECTION_HEADER; -import static com.android.app.search.ResultType.NO_FULFILLMENT; - -import android.app.search.SearchTarget; -import android.os.Bundle; -import android.os.Process; -import android.os.UserHandle; - -public class SearchTargetGenerator { - private static final UserHandle USERHANDLE = Process.myUserHandle(); - - public static SearchTarget EMPTY_DIVIDER_TARGET = - new SearchTarget.Builder(NO_FULFILLMENT, EMPTY_DIVIDER, "divider") - .setPackageName("") /* required but not used*/ - .setUserHandle(USERHANDLE) /* required */ - .setExtras(new Bundle()) - .build(); - - public static SearchTarget SECTION_HEADER_TARGET = - new SearchTarget.Builder(NO_FULFILLMENT, SECTION_HEADER, "section_header") - .setPackageName("") /* required but not used*/ - .setUserHandle(USERHANDLE) /* required */ - .setExtras(new Bundle()) - .build(); -} diff --git a/viewcapturelib/build.gradle b/viewcapturelib/build.gradle index 3f40ad6..e5442cc 100644 --- a/viewcapturelib/build.gradle +++ b/viewcapturelib/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'sysuigradleproject.android-library-conventions' + id 'com.android.library' id 'org.jetbrains.kotlin.android' id 'com.google.protobuf' } @@ -7,12 +7,9 @@ plugins { final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto" android { - compileSdk TARGET_SDK.toInteger() - buildToolsVersion = BUILD_TOOLS_VERSION - + namespace = "com.android.app.viewcapture" + testNamespace = "com.android.app.viewcapture.test" defaultConfig { - minSdkVersion TARGET_SDK.toInteger() - targetSdkVersion TARGET_SDK.toInteger() testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -27,15 +24,15 @@ android { manifest.srcFile "tests/AndroidManifest.xml" } } - - lintOptions { + lint { abortOnError false } + } dependencies { implementation "androidx.core:core:1.9.0" - implementation "com.google.protobuf:protobuf-lite:${protobuf_version}" + implementation "com.google.protobuf:protobuf-lite:${protobuf_lite_version}" androidTestImplementation project(':SharedTestLib') androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation "androidx.test:rules:1.4.0" diff --git a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt index c84d4d5..8a3cf1c 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt +++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt @@ -17,16 +17,22 @@ package com.android.app.viewcapture import android.content.Context +import android.content.pm.LauncherApps import android.database.ContentObserver import android.os.Handler import android.os.Looper +import android.os.ParcelFileDescriptor import android.os.Process import android.provider.Settings +import android.util.Log import android.view.Choreographer +import android.window.IDumpCallback import androidx.annotation.AnyThread import androidx.annotation.VisibleForTesting import java.util.concurrent.Executor +private val TAG = SettingsAwareViewCapture::class.java.simpleName + /** * ViewCapture that listens to system updates and enables / disables attached ViewCapture * WindowListeners accordingly. The Settings toggle is currently controlled by the Winscope @@ -36,6 +42,16 @@ class SettingsAwareViewCapture @VisibleForTesting internal constructor(private val context: Context, choreographer: Choreographer, executor: Executor) : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, choreographer, executor) { + /** Dumps all the active view captures to the wm trace directory via LauncherAppService */ + private val mDumpCallback: IDumpCallback.Stub = object : IDumpCallback.Stub() { + override fun onDump(out: ParcelFileDescriptor) { + try { + ParcelFileDescriptor.AutoCloseOutputStream(out).use { os -> dumpTo(os, context) } + } catch (e: Exception) { + Log.e(TAG, "failed to dump data to wm trace", e) + } + } + } init { enableOrDisableWindowListeners() @@ -57,6 +73,12 @@ internal constructor(private val context: Context, choreographer: Choreographer, MAIN_EXECUTOR.execute { enableOrDisableWindowListeners(isEnabled) } + val launcherApps = context.getSystemService(LauncherApps::class.java) + if (isEnabled) { + launcherApps?.registerDumpCallback(mDumpCallback) + } else { + launcherApps?.unRegisterDumpCallback(mDumpCallback) + } } } diff --git a/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt new file mode 100644 index 0000000..2773f6b --- /dev/null +++ b/viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt @@ -0,0 +1,8 @@ +package com.android.app.viewcapture + +import android.os.Process +import android.view.Choreographer + +open class SimpleViewCapture(threadName: String) : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE, + MAIN_EXECUTOR.submit { Choreographer.getInstance() }.get(), + createAndStartNewLooperExecutor(threadName, Process.THREAD_PRIORITY_FOREGROUND))
\ No newline at end of file diff --git a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java index fcd7ad8..fb5abd6 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java +++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java @@ -16,8 +16,6 @@ package com.android.app.viewcapture; -import static java.util.stream.Collectors.toList; - import android.content.Context; import android.content.res.Resources; import android.media.permission.SafeCloseable; @@ -25,10 +23,6 @@ import android.os.HandlerThread; import android.os.Looper; import android.os.Trace; import android.text.TextUtils; -import android.util.Base64; -import android.util.Base64OutputStream; -import android.util.Log; -import android.util.Pair; import android.util.SparseArray; import android.view.Choreographer; import android.view.View; @@ -36,25 +30,29 @@ import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.view.Window; +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; +import androidx.annotation.VisibleForTesting; import androidx.annotation.WorkerThread; import com.android.app.viewcapture.data.ExportedData; import com.android.app.viewcapture.data.FrameData; +import com.android.app.viewcapture.data.MotionWindowData; import com.android.app.viewcapture.data.ViewNode; +import com.android.app.viewcapture.data.WindowData; -import java.io.FileDescriptor; -import java.io.FileOutputStream; +import java.io.IOException; import java.io.OutputStream; -import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; -import java.util.concurrent.FutureTask; import java.util.function.Consumer; -import java.util.zip.GZIPOutputStream; +import java.util.function.Predicate; /** * Utility class for capturing view data every frame @@ -122,6 +120,7 @@ public abstract class ViewCapture { /** * Attaches the ViewCapture to the provided window and returns a handle to detach the listener */ + @NonNull public SafeCloseable startCapture(Window window) { String title = window.getAttributes().getTitle().toString(); String name = TextUtils.isEmpty(title) ? window.toString() : title; @@ -132,6 +131,7 @@ public abstract class ViewCapture { * Attaches the ViewCapture to the provided window and returns a handle to detach the listener. * Verifies that ViewCapture is enabled before actually attaching an onDrawListener. */ + @NonNull public SafeCloseable startCapture(View view, String name) { WindowListener listener = new WindowListener(view, name); if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot); @@ -142,6 +142,25 @@ public abstract class ViewCapture { }; } + /** + * Launcher checks for leaks in many spots during its instrumented tests. The WindowListeners + * appear to have leaks because they store mRoot views. In reality, attached views close their + * respective window listeners when they are destroyed. + * <p> + * This method deletes detaches and deletes mRoot views from windowListeners. This makes the + * WindowListeners unusable for anything except dumping previously captured information. They + * are still technically enabled to allow for dumping. + */ + @VisibleForTesting + public void stopCapture(@NonNull View rootView) { + mListeners.forEach(it -> { + if (rootView == it.mRoot) { + it.mRoot.getViewTreeObserver().removeOnDrawListener(it); + it.mRoot = null; + } + }); + } + @UiThread protected void enableOrDisableWindowListeners(boolean isEnabled) { mIsEnabled = isEnabled; @@ -149,62 +168,46 @@ public abstract class ViewCapture { if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot); } - - /** - * Dumps all the active view captures - */ - public void dump(PrintWriter writer, FileDescriptor out, Context context) { + @AnyThread + public void dumpTo(OutputStream os, Context context) + throws InterruptedException, ExecutionException, IOException { if (!mIsEnabled) { return; } - ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); + ArrayList<Class> classList = new ArrayList<>(); + ExportedData.newBuilder() + .setPackage(context.getPackageName()) + .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get()) + .addAllClassname(toStringList(classList)) + .build() + .writeTo(os); + } - // Collect all the tasks first so that all the tasks are posted on the executor - List<Pair<String, FutureTask<ExportedData>>> tasks = mListeners.stream() - .map(l -> { - FutureTask<ExportedData> task = - new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider)); - mBgExecutor.execute(task); - return Pair.create(l.name, task); - }) - .collect(toList()); - tasks.forEach(pair -> { - writer.println(); - writer.println(" ContinuousViewCapture:"); - writer.println(" window " + pair.first + ":"); - writer.println(" pkg:" + context.getPackageName()); - writer.print(" data:"); - writer.flush(); - try (OutputStream os = new FileOutputStream(out)) { - ExportedData data = pair.second.get(); - OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os, - Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP)); - data.writeTo(encodedOS); - encodedOS.close(); - os.flush(); - } catch (Exception e) { - Log.e(TAG, "Error capturing proto", e); - } - writer.println(); - writer.println("--end--"); - }); + private static List<String> toStringList(List<Class> classList) { + return classList.stream().map(Class::getName).toList(); } - public Optional<FutureTask<ExportedData>> getDumpTask(View view) { - Context context = view.getContext().getApplicationContext(); - ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); + public CompletableFuture<Optional<MotionWindowData>> getDumpTask(View view) { + ArrayList<Class> classList = new ArrayList<>(); + return getWindowData(view.getContext().getApplicationContext(), classList, + l -> l.mRoot.equals(view)).thenApply(list -> list.stream().findFirst().map(w -> + MotionWindowData.newBuilder() + .addAllFrameData(w.getFrameDataList()) + .addAllClassname(toStringList(classList)) + .build())); + } - return mListeners.stream() - .filter(l -> l.mRoot.equals(view)) - .map(l -> { - FutureTask<ExportedData> task = - new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider)); - mBgExecutor.execute(task); - return task; - }) - .findFirst(); + @AnyThread + private CompletableFuture<List<WindowData>> getWindowData(Context context, + ArrayList<Class> outClassList, Predicate<WindowListener> filter) { + ViewIdProvider idProvider = new ViewIdProvider(context.getResources()); + return CompletableFuture.supplyAsync(() -> + mListeners.stream().filter(filter).toList(), MAIN_EXECUTOR).thenApplyAsync(it -> + it.stream().map(l -> l.dumpToProto(idProvider, outClassList)).toList(), + mBgExecutor); } + /** * Once this window listener is attached to a window's root view, it traverses the entire * view tree on the main thread every time onDraw is called. It then saves the state of the view @@ -245,7 +248,8 @@ public abstract class ViewCapture { */ private class WindowListener implements ViewTreeObserver.OnDrawListener { - public final View mRoot; + @Nullable // Nullable in tests only + public View mRoot; public final String name; private final ViewRef mViewRef = new ViewRef(); @@ -391,7 +395,9 @@ public abstract class ViewCapture { void detachFromRoot() { mIsActive = false; - mRoot.getViewTreeObserver().removeOnDrawListener(this); + if (mRoot != null) { + mRoot.getViewTreeObserver().removeOnDrawListener(this); + } } private void safelyEnableOnDrawListener() { @@ -400,11 +406,9 @@ public abstract class ViewCapture { } @WorkerThread - private ExportedData dumpToProto(ViewIdProvider idProvider) { + private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) { + WindowData.Builder builder = WindowData.newBuilder().setTitle(name); int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize; - ExportedData.Builder exportedDataBuilder = ExportedData.newBuilder(); - ArrayList<Class> classList = new ArrayList<>(); - for (int i = size - 1; i >= 0; i--) { int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize; ViewNode.Builder nodeBuilder = ViewNode.newBuilder(); @@ -412,11 +416,9 @@ public abstract class ViewCapture { FrameData.Builder frameDataBuilder = FrameData.newBuilder() .setNode(nodeBuilder) .setTimestamp(mFrameTimesNanosBg[index]); - exportedDataBuilder.addFrameData(frameDataBuilder); + builder.addFrameData(frameDataBuilder); } - return exportedDataBuilder - .addAllClassname(classList.stream().map(Class::getName).collect(toList())) - .build(); + return builder.build(); } private ViewRef captureViewTree(View view, ViewRef start) { @@ -517,6 +519,8 @@ public abstract class ViewCapture { .setHeight(bottom - top) .setTranslationX(translateX) .setTranslationY(translateY) + .setScrollX(scrollX) + .setScrollY(scrollY) .setScaleX(scaleX) .setScaleY(scaleY) .setAlpha(alpha) diff --git a/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto index 899f678..d4df2ae 100644 --- a/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto +++ b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto @@ -21,7 +21,17 @@ package com.android.app.viewcapture.data; option java_multiple_files = true; message ExportedData { + repeated WindowData windowData = 1; + optional string package = 2; + repeated string classname = 3; +} + +message WindowData { + repeated FrameData frameData = 1; + optional string title = 2; +} +message MotionWindowData { repeated FrameData frameData = 1; repeated string classname = 2; } diff --git a/viewcapturelib/tests/AndroidManifest.xml b/viewcapturelib/tests/AndroidManifest.xml index fb47dd8..8d31c0e 100644 --- a/viewcapturelib/tests/AndroidManifest.xml +++ b/viewcapturelib/tests/AndroidManifest.xml @@ -15,14 +15,13 @@ --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" - package="com.android.app.viewcapture"> - + package="com.android.app.viewcapture.test"> <application android:debuggable="true" android:theme="@android:style/Theme.NoTitleBar"> <activity - android:name=".TestActivity" + android:name="com.android.app.viewcapture.TestActivity" android:exported="false" /> <uses-library android:name="android.test.runner" /> @@ -31,7 +30,7 @@ <instrumentation android:name="android.testing.TestableInstrumentation" - android:label="Tests for ViewCapture Lib" - android:targetPackage="com.android.app.viewcapture" /> + android:label="Tests for MotionTool Lib" + android:targetPackage="com.android.app.viewcapture.test"/> </manifest> diff --git a/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt index e08b549..49d50bf 100644 --- a/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt +++ b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt @@ -16,6 +16,7 @@ package com.android.app.viewcapture +import android.Manifest import android.content.Context import android.content.Intent import android.media.permission.SafeCloseable @@ -26,6 +27,7 @@ import android.view.View import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule import com.android.app.viewcapture.SettingsAwareViewCapture.Companion.VIEW_CAPTURE_ENABLED import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR import junit.framework.Assert.assertEquals @@ -40,6 +42,8 @@ class SettingsAwareViewCaptureTest { private val activityIntent = Intent(context, TestActivity::class.java) @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent) + @get:Rule val grantPermissionRule = + GrantPermissionRule.grant(Manifest.permission.WRITE_SECURE_SETTINGS) @Test fun do_not_capture_view_hierarchies_if_setting_is_disabled() { @@ -48,14 +52,21 @@ class SettingsAwareViewCaptureTest { activityScenarioRule.scenario.onActivity { activity -> val viewCapture: ViewCapture = SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR) - val rootView: View = activity.findViewById(android.R.id.content) + val rootView: View = activity.requireViewById(android.R.id.content) val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") Choreographer.getInstance().postFrameCallback { rootView.viewTreeObserver.dispatchOnDraw() - assertEquals(0, viewCapture.getDumpTask( - activity.findViewById(android.R.id.content)).get().get().frameDataList.size) + assertEquals( + 0, + viewCapture + .getDumpTask(activity.requireViewById(android.R.id.content)) + .get() + .get() + .frameDataList + .size + ) closeable.close() } } @@ -68,14 +79,21 @@ class SettingsAwareViewCaptureTest { activityScenarioRule.scenario.onActivity { activity -> val viewCapture: ViewCapture = SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR) - val rootView: View = activity.findViewById(android.R.id.content) + val rootView: View = activity.requireViewById(android.R.id.content) val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") Choreographer.getInstance().postFrameCallback { rootView.viewTreeObserver.dispatchOnDraw() - assertEquals(1, viewCapture.getDumpTask(activity.findViewById( - android.R.id.content)).get().get().frameDataList.size) + assertEquals( + 1, + viewCapture + .getDumpTask(activity.requireViewById(android.R.id.content)) + .get() + .get() + .frameDataList + .size + ) closeable.close() } diff --git a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt index b0fcca1..c1873a6 100644 --- a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt +++ b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt @@ -27,7 +27,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry import com.android.app.viewcapture.TestActivity.Companion.TEXT_VIEW_COUNT -import com.android.app.viewcapture.data.ExportedData +import com.android.app.viewcapture.data.MotionWindowData import junit.framework.Assert.assertEquals import org.junit.Rule import org.junit.Test @@ -50,40 +50,40 @@ class ViewCaptureTest { @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent) @Test - fun testViewCaptureDumpsOneFrameAfterInvalidate() { + fun testWindowListenerDumpsOneFrameAfterInvalidate() { activityScenarioRule.scenario.onActivity { activity -> Choreographer.getInstance().postFrameCallback { val closeable = startViewCaptureAndInvalidateNTimes(1, activity) - val rootView = activity.findViewById<View>(android.R.id.content) - val exportedData = viewCapture.getDumpTask(rootView).get().get() + val rootView = activity.requireViewById<View>(android.R.id.content) + val data = viewCapture.getDumpTask(rootView).get().get() - assertEquals(1, exportedData.frameDataList.size) - verifyTestActivityViewHierarchy(exportedData) + assertEquals(1, data.frameDataList.size) + verifyTestActivityViewHierarchy(data) closeable.close() } } } @Test - fun testViewCaptureDumpsCorrectlyAfterRecyclingStarted() { + fun testWindowListenerDumpsCorrectlyAfterRecyclingStarted() { activityScenarioRule.scenario.onActivity { activity -> Choreographer.getInstance().postFrameCallback { val closeable = startViewCaptureAndInvalidateNTimes(memorySize + 5, activity) - val rootView = activity.findViewById<View>(android.R.id.content) - val exportedData = viewCapture.getDumpTask(rootView).get().get() + val rootView = activity.requireViewById<View>(android.R.id.content) + val data = viewCapture.getDumpTask(rootView).get().get() // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only // [viewCaptureMemorySize] frames are exported, although the view is invalidated // [viewCaptureMemorySize + 5] times - assertEquals(memorySize, exportedData.frameDataList.size) - verifyTestActivityViewHierarchy(exportedData) + assertEquals(memorySize, data.frameDataList.size) + verifyTestActivityViewHierarchy(data) closeable.close() } } } private fun startViewCaptureAndInvalidateNTimes(n: Int, activity: TestActivity): SafeCloseable { - val rootView: View = activity.findViewById(android.R.id.content) + val rootView: View = activity.requireViewById(android.R.id.content) val closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId") dispatchOnDraw(rootView, times = n) return closeable @@ -96,7 +96,7 @@ class ViewCaptureTest { } } - private fun verifyTestActivityViewHierarchy(exportedData: ExportedData) { + private fun verifyTestActivityViewHierarchy(exportedData: MotionWindowData) { for (frame in exportedData.frameDataList) { val testActivityRoot = frame.node // FrameLayout (android.R.id.content) |