summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:07:29 +0000
committerAndroid Build Coastguard Worker <android-build-coastguard-worker@google.com>2023-07-07 01:07:29 +0000
commit4a27c093ca3d30ef78e25c7009698c3da98b1982 (patch)
treef89956b417badcafc6ed72d30a2507aa4068bb95
parent18ec8059d390a77371c1d3aa6effafc4ac2548fb (diff)
parentac210c97211a51828d906741c22683312f1c1a41 (diff)
downloadsystemui-android14-mainline-tethering-release.tar.gz
Change-Id: I8501bf759b0e59e9a663d5564633ce5df14a9af2
-rw-r--r--animationlib/Android.bp59
-rw-r--r--animationlib/AndroidManifest.xml (renamed from searchuilib/AndroidManifest.xml)4
-rw-r--r--animationlib/TEST_MAPPING7
-rw-r--r--animationlib/build.gradle46
-rw-r--r--animationlib/src/com/android/app/animation/Interpolators.java211
-rw-r--r--animationlib/src/com/android/app/animation/InterpolatorsAndroidX.java218
-rw-r--r--animationlib/src/com/android/app/animation/MathUtils.java27
-rw-r--r--animationlib/tests/AndroidManifest.xml25
-rw-r--r--animationlib/tests/com/android/app/animation/InterpolatorsAndroidXTest.kt54
-rw-r--r--iconloaderlib/Android.bp4
-rw-r--r--iconloaderlib/build.gradle23
-rw-r--r--iconloaderlib/res/drawable/ic_clone_app_badge.xml43
-rw-r--r--iconloaderlib/res/drawable/ic_clone_app_badge_themed.xml43
-rw-r--r--iconloaderlib/res/drawable/ic_instant_app_badge.xml9
-rw-r--r--iconloaderlib/res/drawable/ic_instant_app_badge_themed.xml30
-rw-r--r--iconloaderlib/res/drawable/ic_work_app_badge_themed.xml39
-rw-r--r--iconloaderlib/res/values-night-v31/colors.xml24
-rw-r--r--iconloaderlib/res/values-night/colors.xml24
-rw-r--r--iconloaderlib/res/values-v31/colors.xml24
-rw-r--r--iconloaderlib/res/values/colors.xml4
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java322
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java15
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BubbleIconFactory.java160
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java16
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java15
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java20
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/IconProvider.java2
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java2
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java36
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java15
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java183
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java19
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java3
-rw-r--r--iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java7
-rw-r--r--motiontoollib/.gitignore (renamed from searchuilib/.gitignore)0
-rw-r--r--motiontoollib/Android.bp80
-rw-r--r--motiontoollib/AndroidManifest.xml20
-rw-r--r--motiontoollib/OWNERS3
-rw-r--r--motiontoollib/TEST_MAPPING15
-rw-r--r--motiontoollib/build.gradle63
-rw-r--r--motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt165
-rw-r--r--motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt155
-rw-r--r--motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto113
-rw-r--r--motiontoollib/tests/AndroidManifest.xml37
-rw-r--r--motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt195
-rw-r--r--motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt107
-rw-r--r--motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt21
-rw-r--r--searchuilib/Android.bp28
-rw-r--r--searchuilib/build.gradle35
-rw-r--r--searchuilib/src/com/android/app/search/LayoutType.java79
-rw-r--r--searchuilib/src/com/android/app/search/ResultType.java50
-rw-r--r--viewcapturelib/.gitignore13
-rw-r--r--viewcapturelib/Android.bp73
-rw-r--r--viewcapturelib/AndroidManifest.xml23
-rw-r--r--viewcapturelib/OWNERS2
-rw-r--r--viewcapturelib/README.md11
-rw-r--r--viewcapturelib/TEST_MAPPING15
-rw-r--r--viewcapturelib/build.gradle62
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java59
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt105
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/SimpleViewCapture.kt8
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java614
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto69
-rw-r--r--viewcapturelib/tests/AndroidManifest.xml36
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt96
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt45
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt116
67 files changed, 3827 insertions, 419 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/Android.bp b/iconloaderlib/Android.bp
index 6867e6b..083091d 100644
--- a/iconloaderlib/Android.bp
+++ b/iconloaderlib/Android.bp
@@ -45,4 +45,8 @@ android_library {
"src/**/*.java",
"src_full_lib/**/*.java",
],
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.permission",
+ ],
}
diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle
index 10ec889..344ac20 100644
--- a/iconloaderlib/build.gradle
+++ b/iconloaderlib/build.gradle
@@ -1,14 +1,9 @@
-apply plugin: 'com.android.library'
+plugins {
+ id 'com.android.library'
+}
android {
- compileSdkVersion COMPILE_SDK
- buildToolsVersion BUILD_TOOLS_VERSION
-
- defaultConfig {
- minSdkVersion 26
- targetSdkVersion 28
- }
-
+ namespace = "com.android.launcher3.icons"
sourceSets {
main {
java.srcDirs = ['src', 'src_full_lib']
@@ -16,21 +11,15 @@ android {
res.srcDirs = ['res']
}
}
-
- lintOptions {
+ lint {
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:${ANDROID_X_VERSION}"
+ implementation "androidx.core:core"
}
diff --git a/iconloaderlib/res/drawable/ic_clone_app_badge.xml b/iconloaderlib/res/drawable/ic_clone_app_badge.xml
new file mode 100644
index 0000000..9f0876d
--- /dev/null
+++ b/iconloaderlib/res/drawable/ic_clone_app_badge.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="@android:color/white"
+ 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="#ff3C4043"/>
+ <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="#ff3C4043"
+ android:fillType="evenOdd"/>
+ </group>
+</vector>
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/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index c0be55d..704df6f 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -1,13 +1,17 @@
package com.android.launcher3.icons;
+import static android.graphics.Paint.ANTI_ALIAS_FLAG;
import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
+import static com.android.launcher3.icons.BitmapInfo.FLAG_CLONE;
import static com.android.launcher3.icons.BitmapInfo.FLAG_INSTANT;
import static com.android.launcher3.icons.BitmapInfo.FLAG_WORK;
import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.annotation.TargetApi;
import android.content.Context;
import android.content.Intent;
@@ -30,12 +34,17 @@ import android.os.Build;
import android.os.UserHandle;
import android.util.SparseBooleanArray;
+import androidx.annotation.ColorInt;
+import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.icons.BitmapInfo.Extender;
import com.android.launcher3.util.FlagOp;
+import java.lang.annotation.Retention;
+import java.util.Objects;
+
/**
* This class will be moved to androidx library. There shouldn't be any dependency outside
* this package.
@@ -44,23 +53,47 @@ public class BaseIconFactory implements AutoCloseable {
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
+ public static final int MODE_DEFAULT = 0;
+ public static final int MODE_ALPHA = 1;
+ public static final int MODE_WITH_SHADOW = 2;
+ public static final int MODE_HARDWARE = 3;
+ public static final int MODE_HARDWARE_WITH_SHADOW = 4;
+
+ @Retention(SOURCE)
+ @IntDef({MODE_DEFAULT, MODE_ALPHA, MODE_WITH_SHADOW, MODE_HARDWARE_WITH_SHADOW, MODE_HARDWARE})
+ @interface BitmapGenerationMode {}
+
private static final float ICON_BADGE_SCALE = 0.444f;
+ @NonNull
private final Rect mOldBounds = new Rect();
+
+ @NonNull
private final SparseBooleanArray mIsUserBadged = new SparseBooleanArray();
+
+ @NonNull
protected final Context mContext;
+
+ @NonNull
private final Canvas mCanvas;
+
+ @NonNull
private final PackageManager mPm;
+
+ @NonNull
private final ColorExtractor mColorExtractor;
- private boolean mDisableColorExtractor;
protected final int mFillResIconDpi;
protected final int mIconBitmapSize;
protected boolean mMonoIconEnabled;
+ @Nullable
private IconNormalizer mNormalizer;
+
+ @Nullable
private ShadowGenerator mShadowGenerator;
+
private final boolean mShapeDetection;
// Shadow bitmap used as background for theme icons
@@ -69,8 +102,6 @@ public class BaseIconFactory implements AutoCloseable {
private Drawable mWrapperIcon;
private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
- private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
- private static final float PLACEHOLDER_TEXT_SIZE = 20f;
private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(245, 245, 245);
protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
@@ -85,10 +116,6 @@ public class BaseIconFactory implements AutoCloseable {
mCanvas = new Canvas();
mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
- mTextPaint.setTextAlign(Paint.Align.CENTER);
- mTextPaint.setColor(PLACEHOLDER_BACKGROUND_COLOR);
- mTextPaint.setTextSize(context.getResources().getDisplayMetrics().density *
- PLACEHOLDER_TEXT_SIZE);
clear();
}
@@ -98,9 +125,9 @@ public class BaseIconFactory implements AutoCloseable {
protected void clear() {
mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
- mDisableColorExtractor = false;
}
+ @NonNull
public ShadowGenerator getShadowGenerator() {
if (mShadowGenerator == null) {
mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
@@ -108,6 +135,7 @@ public class BaseIconFactory implements AutoCloseable {
return mShadowGenerator;
}
+ @NonNull
public IconNormalizer getNormalizer() {
if (mNormalizer == null) {
mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
@@ -138,16 +166,11 @@ public class BaseIconFactory implements AutoCloseable {
* @return
*/
public BitmapInfo createIconBitmap(String placeholder, int color) {
- Bitmap placeholderBitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize,
- Bitmap.Config.ARGB_8888);
- mTextPaint.setColor(color);
- Canvas canvas = new Canvas(placeholderBitmap);
- canvas.drawText(placeholder, mIconBitmapSize / 2, mIconBitmapSize * 5 / 8, mTextPaint);
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
- new BitmapDrawable(mContext.getResources(), placeholderBitmap));
+ new CenterTextDrawable(placeholder, color));
Bitmap icon = createIconBitmap(drawable, IconNormalizer.ICON_VISIBLE_AREA_FACTOR);
- return BitmapInfo.of(icon, extractColor(icon));
+ return BitmapInfo.of(icon, color);
}
public BitmapInfo createIconBitmap(Bitmap icon) {
@@ -155,12 +178,13 @@ public class BaseIconFactory implements AutoCloseable {
icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
}
- return BitmapInfo.of(icon, extractColor(icon));
+ return BitmapInfo.of(icon, mColorExtractor.findDominantColorByHue(icon));
}
/**
* Creates an icon from the bitmap cropped to the current device icon shape
*/
+ @NonNull
public BitmapInfo createShapedIconBitmap(Bitmap icon, IconOptions options) {
Drawable d = new FixedSizeBitmapDrawable(icon);
float inset = getExtraInsetFraction();
@@ -170,6 +194,7 @@ public class BaseIconFactory implements AutoCloseable {
return createBadgedIconBitmap(d, options);
}
+ @NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon) {
return createBadgedIconBitmap(icon, null);
}
@@ -182,38 +207,47 @@ public class BaseIconFactory implements AutoCloseable {
* @return a bitmap suitable for disaplaying as an icon at various system UIs.
*/
@TargetApi(Build.VERSION_CODES.TIRAMISU)
+ @NonNull
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon,
@Nullable IconOptions options) {
boolean shrinkNonAdaptiveIcons = options == null || options.mShrinkNonAdaptiveIcons;
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
- Bitmap bitmap = createIconBitmap(icon, scale[0]);
- if (icon instanceof AdaptiveIconDrawable) {
- mCanvas.setBitmap(bitmap);
- getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
- mCanvas.setBitmap(null);
- }
+ Bitmap bitmap = createIconBitmap(icon, scale[0],
+ options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
- int color = extractColor(bitmap);
+ int color = (options != null && options.mExtractedColor != null)
+ ? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
BitmapInfo info = BitmapInfo.of(bitmap, color);
if (icon instanceof BitmapInfo.Extender) {
info = ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0]);
- } else if (mMonoIconEnabled && IconProvider.ATLEAST_T
- && icon instanceof AdaptiveIconDrawable) {
- Drawable mono = ((AdaptiveIconDrawable) icon).getMonochrome();
+ } else if (IconProvider.ATLEAST_T && mMonoIconEnabled) {
+ Drawable mono = getMonochromeDrawable(icon);
if (mono != null) {
- // Convert mono drawable to bitmap
- Drawable paddedMono = new ClippedMonoDrawable(mono);
- info.setMonoIcon(
- createIconBitmap(paddedMono, scale[0], mIconBitmapSize, Config.ALPHA_8),
- this);
+ info.setMonoIcon(createIconBitmap(mono, scale[0], MODE_ALPHA), this);
}
}
info = info.withFlags(getBitmapFlagOp(options));
return info;
}
+ /**
+ * Returns a monochromatic version of the given drawable or null, if it is not supported
+ * @param base the original icon
+ */
+ @TargetApi(Build.VERSION_CODES.TIRAMISU)
+ protected Drawable getMonochromeDrawable(Drawable base) {
+ if (base instanceof AdaptiveIconDrawable) {
+ Drawable mono = ((AdaptiveIconDrawable) base).getMonochrome();
+ if (mono != null) {
+ return new ClippedMonoDrawable(mono);
+ }
+ }
+ return null;
+ }
+
+ @NonNull
public FlagOp getBitmapFlagOp(@Nullable IconOptions options) {
FlagOp op = FlagOp.NO_OP;
if (options != null) {
@@ -233,55 +267,45 @@ public class BaseIconFactory implements AutoCloseable {
isBadged = (d != mPm.getUserBadgedIcon(d, options.mUserHandle));
mIsUserBadged.put(key, isBadged);
}
- op = op.setFlag(FLAG_WORK, isBadged);
+ // Set the clone profile badge flag in case it is present.
+ op = op.setFlag(FLAG_CLONE, isBadged && options.mIsCloneProfile);
+ // Set the Work profile badge for all other cases.
+ op = op.setFlag(FLAG_WORK, isBadged && !options.mIsCloneProfile);
}
}
return op;
}
- /** package private */
- Bitmap getWhiteShadowLayer() {
+ @NonNull
+ public Bitmap getWhiteShadowLayer() {
if (mWhiteShadowLayer == null) {
- mWhiteShadowLayer = createScaledBitmapWithShadow(
- new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null));
+ mWhiteShadowLayer = createScaledBitmap(
+ new AdaptiveIconDrawable(new ColorDrawable(Color.WHITE), null),
+ MODE_HARDWARE_WITH_SHADOW);
}
return mWhiteShadowLayer;
}
- /** package private */
- public Bitmap createScaledBitmapWithShadow(Drawable d) {
- float scale = getNormalizer().getScale(d, null, null, null);
- Bitmap bitmap = createIconBitmap(d, scale);
- mCanvas.setBitmap(bitmap);
- getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
- mCanvas.setBitmap(null);
- return bitmap;
- }
-
- public Bitmap createScaledBitmapWithoutShadow(Drawable icon) {
+ @NonNull
+ public Bitmap createScaledBitmap(@NonNull Drawable icon, @BitmapGenerationMode int mode) {
RectF iconBounds = new RectF();
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, true, iconBounds, scale);
return createIconBitmap(icon,
- Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
+ Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)), mode);
}
/**
* Sets the background color used for wrapped adaptive icon
*/
- public void setWrapperBackgroundColor(int color) {
+ public void setWrapperBackgroundColor(final int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
- /**
- * Disables the dominant color extraction for all icons loaded.
- */
- public void disableColorExtraction() {
- mDisableColorExtractor = true;
- }
-
- private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
- boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
+ @Nullable
+ protected Drawable normalizeAndWrapToAdaptiveIcon(@Nullable Drawable icon,
+ final boolean shrinkNonAdaptiveIcons, @Nullable final RectF outIconBounds,
+ @NonNull final float[] outScale) {
if (icon == null) {
return null;
}
@@ -312,46 +336,69 @@ public class BaseIconFactory implements AutoCloseable {
return icon;
}
- private Bitmap createIconBitmap(Drawable icon, float scale) {
- return createIconBitmap(icon, scale, mIconBitmapSize);
- }
-
- /**
- * @param icon drawable that should be flattened to a bitmap
- * @param scale the scale to apply before drawing {@param icon} on the canvas
- */
- public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
- return createIconBitmap(icon, scale, size, Bitmap.Config.ARGB_8888);
+ @NonNull
+ protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale) {
+ return createIconBitmap(icon, scale, MODE_DEFAULT);
}
- private Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size,
- Bitmap.Config config) {
- Bitmap bitmap = Bitmap.createBitmap(size, size, config);
+ @NonNull
+ protected Bitmap createIconBitmap(@Nullable final Drawable icon, final float scale,
+ @BitmapGenerationMode int bitmapGenerationMode) {
+ final int size = mIconBitmapSize;
+ final Bitmap bitmap;
+ switch (bitmapGenerationMode) {
+ case MODE_ALPHA:
+ bitmap = Bitmap.createBitmap(size, size, Config.ALPHA_8);
+ break;
+ case MODE_HARDWARE:
+ case MODE_HARDWARE_WITH_SHADOW: {
+ return BitmapRenderer.createHardwareBitmap(size, size, canvas ->
+ drawIconBitmap(canvas, icon, scale, bitmapGenerationMode, null));
+ }
+ case MODE_WITH_SHADOW:
+ default:
+ bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
+ break;
+ }
if (icon == null) {
return bitmap;
}
mCanvas.setBitmap(bitmap);
+ drawIconBitmap(mCanvas, icon, scale, bitmapGenerationMode, bitmap);
+ mCanvas.setBitmap(null);
+ return bitmap;
+ }
+
+ private void drawIconBitmap(@NonNull Canvas canvas, @Nullable final Drawable icon,
+ final float scale, @BitmapGenerationMode int bitmapGenerationMode,
+ @Nullable Bitmap targetBitmap) {
+ final int size = mIconBitmapSize;
mOldBounds.set(icon.getBounds());
if (icon instanceof AdaptiveIconDrawable) {
int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
- Math.round(size * (1 - scale) / 2 ));
+ Math.round(size * (1 - scale) / 2));
// b/211896569: AdaptiveIconDrawable do not work properly for non top-left bounds
icon.setBounds(0, 0, size - offset - offset, size - offset - offset);
- int count = mCanvas.save();
- mCanvas.translate(offset, offset);
+ int count = canvas.save();
+ canvas.translate(offset, offset);
+ if (bitmapGenerationMode == MODE_WITH_SHADOW
+ || bitmapGenerationMode == MODE_HARDWARE_WITH_SHADOW) {
+ getShadowGenerator().addPathShadow(
+ ((AdaptiveIconDrawable) icon).getIconMask(), canvas);
+ }
if (icon instanceof BitmapInfo.Extender) {
- ((Extender) icon).drawForPersistence(mCanvas);
+ ((Extender) icon).drawForPersistence(canvas);
} else {
- icon.draw(mCanvas);
+ icon.draw(canvas);
}
- mCanvas.restoreToCount(count);
+ canvas.restoreToCount(count);
} else {
if (icon instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
Bitmap b = bitmapDrawable.getBitmap();
- if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
+ if (b != null && b.getDensity() == Bitmap.DENSITY_NONE) {
bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
}
}
@@ -372,15 +419,24 @@ public class BaseIconFactory implements AutoCloseable {
final int left = (size - width) / 2;
final int top = (size - height) / 2;
icon.setBounds(left, top, left + width, top + height);
- mCanvas.save();
- mCanvas.scale(scale, scale, size / 2, size / 2);
- icon.draw(mCanvas);
- mCanvas.restore();
+ canvas.save();
+ canvas.scale(scale, scale, size / 2, size / 2);
+ icon.draw(canvas);
+ canvas.restore();
+
+ if (bitmapGenerationMode == MODE_WITH_SHADOW && targetBitmap != null) {
+ // Shadow extraction only works in software mode
+ getShadowGenerator().drawShadow(targetBitmap, canvas);
+
+ // Draw the icon again on top:
+ canvas.save();
+ canvas.scale(scale, scale, size / 2, size / 2);
+ icon.draw(canvas);
+ canvas.restore();
+ }
}
icon.setBounds(mOldBounds);
- mCanvas.setBitmap(null);
- return bitmap;
}
@Override
@@ -388,36 +444,45 @@ public class BaseIconFactory implements AutoCloseable {
clear();
}
+ @NonNull
public BitmapInfo makeDefaultIcon() {
return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi));
}
- public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
- return Resources.getSystem().getDrawableForDensity(
- android.R.drawable.sym_def_app_icon, iconDpi);
- }
-
- private int extractColor(Bitmap bitmap) {
- return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
+ @NonNull
+ public static Drawable getFullResDefaultActivityIcon(final int iconDpi) {
+ return Objects.requireNonNull(Resources.getSystem().getDrawableForDensity(
+ android.R.drawable.sym_def_app_icon, iconDpi));
}
/**
* Returns the correct badge size given an icon size
*/
- public static int getBadgeSizeForIconSize(int iconSize) {
+ public static int getBadgeSizeForIconSize(final int iconSize) {
return (int) (ICON_BADGE_SCALE * iconSize);
}
public static class IconOptions {
boolean mShrinkNonAdaptiveIcons = true;
+
boolean mIsInstantApp;
- UserHandle mUserHandle;
+
+ boolean mIsCloneProfile;
+
+ @BitmapGenerationMode
+ int mGenerationMode = MODE_WITH_SHADOW;
+
+ @Nullable UserHandle mUserHandle;
+
+ @ColorInt
+ @Nullable Integer mExtractedColor;
/**
* Set to false if non-adaptive icons should not be treated
*/
- public IconOptions setShrinkNonAdaptiveIcons(boolean shrink) {
+ @NonNull
+ public IconOptions setShrinkNonAdaptiveIcons(final boolean shrink) {
mShrinkNonAdaptiveIcons = shrink;
return this;
}
@@ -425,7 +490,8 @@ public class BaseIconFactory implements AutoCloseable {
/**
* User for this icon, in case of badging
*/
- public IconOptions setUser(UserHandle user) {
+ @NonNull
+ public IconOptions setUser(@Nullable final UserHandle user) {
mUserHandle = user;
return this;
}
@@ -433,10 +499,39 @@ public class BaseIconFactory implements AutoCloseable {
/**
* If this icon represents an instant app
*/
- public IconOptions setInstantApp(boolean instantApp) {
+ @NonNull
+ public IconOptions setInstantApp(final boolean instantApp) {
mIsInstantApp = instantApp;
return this;
}
+
+ /**
+ * Disables auto color extraction and overrides the color to the provided value
+ */
+ @NonNull
+ public IconOptions setExtractedColor(@ColorInt int color) {
+ mExtractedColor = color;
+ return this;
+ }
+
+ /**
+ * Sets the bitmap generation mode to use for the bitmap info. Note that some generation
+ * modes do not support color extraction, so consider setting a extracted color manually
+ * in those cases.
+ */
+ public IconOptions setBitmapGenerationMode(@BitmapGenerationMode int generationMode) {
+ mGenerationMode = generationMode;
+ return this;
+ }
+
+ /**
+ * Used to determine the badge type for this icon.
+ */
+ @NonNull
+ public IconOptions setIsCloneProfile(boolean isCloneProfile) {
+ mIsCloneProfile = isCloneProfile;
+ return this;
+ }
}
/**
@@ -446,7 +541,7 @@ public class BaseIconFactory implements AutoCloseable {
*/
private static class FixedSizeBitmapDrawable extends BitmapDrawable {
- public FixedSizeBitmapDrawable(Bitmap bitmap) {
+ public FixedSizeBitmapDrawable(@Nullable final Bitmap bitmap) {
super(null, bitmap);
}
@@ -473,11 +568,12 @@ public class BaseIconFactory implements AutoCloseable {
}
}
- private static class ClippedMonoDrawable extends InsetDrawable {
+ protected static class ClippedMonoDrawable extends InsetDrawable {
+ @NonNull
private final AdaptiveIconDrawable mCrop;
- public ClippedMonoDrawable(Drawable base) {
+ public ClippedMonoDrawable(@Nullable final Drawable base) {
super(base, -getExtraInsetFraction());
mCrop = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
}
@@ -491,4 +587,32 @@ public class BaseIconFactory implements AutoCloseable {
canvas.restoreToCount(saveCount);
}
}
+
+ private static class CenterTextDrawable extends ColorDrawable {
+
+ @NonNull
+ private final Rect mTextBounds = new Rect();
+
+ @NonNull
+ private final Paint mTextPaint = new Paint(ANTI_ALIAS_FLAG | FILTER_BITMAP_FLAG);
+
+ @NonNull
+ private final String mText;
+
+ CenterTextDrawable(@NonNull final String text, final int color) {
+ mText = text;
+ mTextPaint.setColor(color);
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ Rect bounds = getBounds();
+ mTextPaint.setTextSize(bounds.height() / 3f);
+ mTextPaint.getTextBounds(mText, 0, mText.length(), mTextBounds);
+ canvas.drawText(mText,
+ bounds.exactCenterX() - mTextBounds.exactCenterX(),
+ bounds.exactCenterY() - mTextBounds.exactCenterY(),
+ mTextPaint);
+ }
+ }
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index c3ca42e..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;
@@ -30,9 +31,11 @@ public class BitmapInfo {
static final int FLAG_WORK = 1 << 0;
static final int FLAG_INSTANT = 1 << 1;
+ static final int FLAG_CLONE = 1 << 2;
@IntDef(flag = true, value = {
FLAG_WORK,
FLAG_INSTANT,
+ FLAG_CLONE
})
@interface BitmapInfoFlags {}
@@ -152,9 +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(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/ClockDrawableWrapper.java b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
index d624805..252c0c3 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ClockDrawableWrapper.java
@@ -214,7 +214,8 @@ public class ClockDrawableWrapper extends AdaptiveIconDrawable implements Bitmap
BaseIconFactory iconFactory, float normalizationScale) {
AdaptiveIconDrawable background = new AdaptiveIconDrawable(
getBackground().getConstantState().newDrawable(), null);
- Bitmap flattenBG = iconFactory.createScaledBitmapWithShadow(background);
+ Bitmap flattenBG = iconFactory.createScaledBitmap(background,
+ BaseIconFactory.MODE_HARDWARE_WITH_SHADOW);
// Only pass theme info if mono-icon is enabled
AnimationInfo themeInfo = iconFactory.mMonoIconEnabled ? mThemeInfo : null;
@@ -387,7 +388,8 @@ public class ClockDrawableWrapper extends AdaptiveIconDrawable implements Bitmap
mBgPaint.setColorFilter(cs.mBgFilter);
mThemedFgColor = cs.mThemedFgColor;
- mFullDrawable = (AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable();
+ mFullDrawable =
+ (AdaptiveIconDrawable) mAnimInfo.baseDrawableState.newDrawable().mutate();
mFG = (LayerDrawable) mFullDrawable.getForeground();
// Time needs to be applied here since drawInternal is NOT guaranteed to be called
@@ -397,6 +399,13 @@ public class ClockDrawableWrapper extends AdaptiveIconDrawable implements Bitmap
}
@Override
+ public void setAlpha(int alpha) {
+ super.setAlpha(alpha);
+ mBgPaint.setAlpha(alpha);
+ mFG.setAlpha(alpha);
+ }
+
+ @Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
@@ -434,8 +443,7 @@ public class ClockDrawableWrapper extends AdaptiveIconDrawable implements Bitmap
protected void updateFilter() {
super.updateFilter();
int alpha = mIsDisabled ? (int) (mDisabledAlpha * FULLY_OPAQUE) : FULLY_OPAQUE;
- mBgPaint.setAlpha(alpha);
- mFG.setAlpha(alpha);
+ setAlpha(alpha);
mBgPaint.setColorFilter(mIsDisabled ? getDisabledColorFilter() : mBgFilter);
mFG.setColorFilter(mIsDisabled ? getDisabledColorFilter() : null);
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
index 87bda82..5a5e7d0 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ColorExtractor.java
@@ -18,6 +18,9 @@ package com.android.launcher3.icons;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.util.SparseArray;
+
+import androidx.annotation.NonNull;
+
import java.util.Arrays;
/**
@@ -26,16 +29,24 @@ import java.util.Arrays;
public class ColorExtractor {
private final int NUM_SAMPLES = 20;
+
+ @NonNull
private final float[] mTmpHsv = new float[3];
+
+ @NonNull
private final float[] mTmpHueScoreHistogram = new float[360];
+
+ @NonNull
private final int[] mTmpPixels = new int[NUM_SAMPLES];
+
+ @NonNull
private final SparseArray<Float> mTmpRgbScores = new SparseArray<>();
/**
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
* @param bitmap The bitmap to scan
*/
- public int findDominantColorByHue(Bitmap bitmap) {
+ public int findDominantColorByHue(@NonNull final Bitmap bitmap) {
return findDominantColorByHue(bitmap, NUM_SAMPLES);
}
@@ -43,7 +54,7 @@ public class ColorExtractor {
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
* @param bitmap The bitmap to scan
*/
- public int findDominantColorByHue(Bitmap bitmap, int samples) {
+ protected int findDominantColorByHue(@NonNull final Bitmap bitmap, final int samples) {
final int height = bitmap.getHeight();
final int width = bitmap.getWidth();
int sampleStride = (int) Math.sqrt((height * width) / samples);
diff --git a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
index 17b0016..3455dba 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/GraphicsUtils.java
@@ -16,9 +16,11 @@
package com.android.launcher3.icons;
import android.content.Context;
+import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Color;
+import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
@@ -28,6 +30,8 @@ import android.graphics.drawable.ColorDrawable;
import android.util.Log;
import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.core.graphics.PathParser;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -35,6 +39,7 @@ import java.io.IOException;
public class GraphicsUtils {
private static final String TAG = "GraphicsUtils";
+ private static final float MASK_SIZE = 100f;
public static Runnable sOnNewBitmapRunnable = () -> { };
@@ -98,7 +103,20 @@ public class GraphicsUtils {
/**
* Returns the default path to be used by an icon
*/
- public static Path getShapePath(int size) {
+ public static Path getShapePath(@NonNull Context context, int size) {
+ if (IconProvider.CONFIG_ICON_MASK_RES_ID != Resources.ID_NULL) {
+ Path path = PathParser.createPathFromPathData(
+ context.getString(IconProvider.CONFIG_ICON_MASK_RES_ID));
+ if (path != null) {
+ if (size != MASK_SIZE) {
+ Matrix m = new Matrix();
+ float scale = ((float) size) / MASK_SIZE;
+ m.setScale(scale, scale);
+ path.transform(m);
+ }
+ return path;
+ }
+ }
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(Color.BLACK), new ColorDrawable(Color.BLACK));
drawable.setBounds(0, 0, size, size);
diff --git a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
index 204651c..e8ce3b1 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -61,7 +61,7 @@ import java.util.function.Supplier;
public class IconProvider {
private final String ACTION_OVERLAY_CHANGED = "android.intent.action.OVERLAY_CHANGED";
- private static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
+ static final int CONFIG_ICON_MASK_RES_ID = Resources.getSystem().getIdentifier(
"config_icon_mask", "string", "android");
private static final String TAG = "IconProvider";
diff --git a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
index 5f3343e..71a80cb 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/PlaceHolderIconDrawable.java
@@ -40,7 +40,7 @@ public class PlaceHolderIconDrawable extends FastBitmapDrawable {
public PlaceHolderIconDrawable(BitmapInfo info, Context context) {
super(info);
- mProgressPath = GraphicsUtils.getShapePath(100);
+ mProgressPath = GraphicsUtils.getShapePath(context, 100);
mPaint.setColor(ColorUtils.compositeColors(
GraphicsUtils.getAttrColor(context, R.attr.loadingIconColor), info.color));
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
index 96dee3b..99f6813 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ShadowGenerator.java
@@ -24,6 +24,7 @@ import android.graphics.BlurMaskFilter.Blur;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
+import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
@@ -57,30 +58,41 @@ public class ShadowGenerator {
mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL);
}
- public synchronized void recreateIcon(Bitmap icon, Canvas out) {
- recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
- }
-
- public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
- int ambientAlpha, int keyAlpha, Canvas out) {
+ public synchronized void drawShadow(Bitmap icon, Canvas out) {
if (ENABLE_SHADOWS) {
int[] offset = new int[2];
- mBlurPaint.setMaskFilter(blurMaskFilter);
+ mBlurPaint.setMaskFilter(mDefaultBlurMaskFilter);
Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
// Draw ambient shadow
- mDrawPaint.setAlpha(ambientAlpha);
+ mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
// Draw key shadow
- mDrawPaint.setAlpha(keyAlpha);
+ mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize,
mDrawPaint);
}
+ }
+
+ /** package private **/
+ void addPathShadow(Path path, Canvas out) {
+ if (ENABLE_SHADOWS) {
+ mDrawPaint.setMaskFilter(mDefaultBlurMaskFilter);
- // Draw the icon
- mDrawPaint.setAlpha(255);
- out.drawBitmap(icon, 0, 0, mDrawPaint);
+ // Draw ambient shadow
+ mDrawPaint.setAlpha(AMBIENT_SHADOW_ALPHA);
+ out.drawPath(path, mDrawPaint);
+
+ // Draw key shadow
+ int save = out.save();
+ mDrawPaint.setAlpha(KEY_SHADOW_ALPHA);
+ out.translate(0, KEY_SHADOW_DISTANCE * mIconSize);
+ out.drawPath(path, mDrawPaint);
+ out.restoreToCount(save);
+
+ mDrawPaint.setMaskFilter(null);
+ }
}
/**
diff --git a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
index 494d657..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;
@@ -92,8 +93,11 @@ public class ThemedIconDrawable extends FastBitmapDrawable {
return new ThemedConstantState(bitmapInfo, colorBg, colorFg);
}
- public void changeBackgroundColor(int colorBg){
+ public void changeBackgroundColor(int colorBg) {
+ if (mIsDisabled) return;
+
mBgPaint.setColorFilter(new BlendModeColorFilter(colorBg, BlendMode.SRC_IN));
+ invalidateSelf();
}
static class ThemedConstantState extends FastBitmapConstantState {
@@ -125,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/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 057bdc2..bdc4410 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
@@ -45,6 +45,7 @@ import android.os.Handler;
import android.os.LocaleList;
import android.os.Looper;
import android.os.Process;
+import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.text.TextUtils;
@@ -54,6 +55,7 @@ import android.util.SparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BaseIconFactory.IconOptions;
@@ -78,6 +80,8 @@ public abstract class BaseIconCache {
private static final boolean DEBUG = false;
private static final int INITIAL_ICON_CACHE_CAPACITY = 50;
+ // A format string which returns the original string as is.
+ private static final String IDENTITY_FORMAT_STRING = "%1$s";
// Empty class name is used for storing package default entry.
public static final String EMPTY_CLASS_NAME = ".";
@@ -86,29 +90,52 @@ public abstract class BaseIconCache {
@NonNull
public BitmapInfo bitmap = BitmapInfo.LOW_RES_INFO;
+ @NonNull
public CharSequence title = "";
+ @NonNull
public CharSequence contentDescription = "";
}
+ @NonNull
protected final Context mContext;
+
+ @NonNull
protected final PackageManager mPackageManager;
+ @NonNull
private final Map<ComponentKey, CacheEntry> mCache;
+
+ @NonNull
protected final Handler mWorkerHandler;
protected int mIconDpi;
+
+ @NonNull
protected IconDB mIconDb;
+
+ @NonNull
protected LocaleList mLocaleList = LocaleList.getEmptyLocaleList();
+
+ @NonNull
protected String mSystemState = "";
+ @Nullable
private BitmapInfo mDefaultIcon;
+
+ @NonNull
private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
+ private final SparseArray<String> mUserFormatString = new SparseArray<>();
+
+ @Nullable
private final String mDbFileName;
+
+ @NonNull
private final Looper mBgLooper;
- public BaseIconCache(Context context, String dbFileName, Looper bgLooper,
- int iconDpi, int iconPixelSize, boolean inMemoryCache) {
+ public BaseIconCache(@NonNull final Context context, @Nullable final String dbFileName,
+ @NonNull final Looper bgLooper, final int iconDpi, final int iconPixelSize,
+ final boolean inMemoryCache) {
mContext = context;
mDbFileName = dbFileName;
mPackageManager = context.getPackageManager();
@@ -141,23 +168,24 @@ public abstract class BaseIconCache {
* Returns the persistable serial number for {@param user}. Subclass should implement proper
* caching strategy to avoid making binder call every time.
*/
- protected abstract long getSerialNumberForUser(UserHandle user);
+ protected abstract long getSerialNumberForUser(@NonNull final UserHandle user);
/**
* Return true if the given app is an instant app and should be badged appropriately.
*/
- protected abstract boolean isInstantApp(ApplicationInfo info);
+ protected abstract boolean isInstantApp(@NonNull final ApplicationInfo info);
/**
* Opens and returns an icon factory. The factory is recycled by the caller.
*/
+ @NonNull
public abstract BaseIconFactory getIconFactory();
- public void updateIconParams(int iconDpi, int iconPixelSize) {
+ public void updateIconParams(final int iconDpi, final int iconPixelSize) {
mWorkerHandler.post(() -> updateIconParamsBg(iconDpi, iconPixelSize));
}
- private synchronized void updateIconParamsBg(int iconDpi, int iconPixelSize) {
+ private synchronized void updateIconParamsBg(final int iconDpi, final int iconPixelSize) {
mIconDpi = iconDpi;
mDefaultIcon = null;
mUserFlagOpMap.clear();
@@ -167,7 +195,8 @@ public abstract class BaseIconCache {
mCache.clear();
}
- private Drawable getFullResIcon(Resources resources, int iconId) {
+ @Nullable
+ private Drawable getFullResIcon(@Nullable final Resources resources, final int iconId) {
if (resources != null && iconId != 0) {
try {
return resources.getDrawableForDensity(iconId, mIconDpi);
@@ -176,14 +205,16 @@ public abstract class BaseIconCache {
return getFullResDefaultActivityIcon(mIconDpi);
}
- public Drawable getFullResIcon(String packageName, int iconId) {
+ @Nullable
+ public Drawable getFullResIcon(@NonNull final String packageName, final int iconId) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(packageName), iconId);
} catch (PackageManager.NameNotFoundException e) { }
return getFullResDefaultActivityIcon(mIconDpi);
}
- public Drawable getFullResIcon(ActivityInfo info) {
+ @Nullable
+ public Drawable getFullResIcon(@NonNull final ActivityInfo info) {
try {
return getFullResIcon(mPackageManager.getResourcesForApplication(info.applicationInfo),
info.getIconResource());
@@ -194,14 +225,16 @@ public abstract class BaseIconCache {
/**
* Remove any records for the supplied ComponentName.
*/
- public synchronized void remove(ComponentName componentName, UserHandle user) {
+ public synchronized void remove(@NonNull final ComponentName componentName,
+ @NonNull final UserHandle user) {
mCache.remove(new ComponentKey(componentName, user));
}
/**
* Remove any records for the supplied package name from memory.
*/
- private void removeFromMemCacheLocked(String packageName, UserHandle user) {
+ private void removeFromMemCacheLocked(@Nullable final String packageName,
+ @Nullable final UserHandle user) {
HashSet<ComponentKey> forDeletion = new HashSet<>();
for (ComponentKey key: mCache.keySet()) {
if (key.componentName.getPackageName().equals(packageName)
@@ -217,7 +250,8 @@ public abstract class BaseIconCache {
/**
* Removes the entries related to the given package in memory and persistent DB.
*/
- public synchronized void removeIconsForPkg(String packageName, UserHandle user) {
+ public synchronized void removeIconsForPkg(@NonNull final String packageName,
+ @NonNull final UserHandle user) {
removeFromMemCacheLocked(packageName, user);
long userSerial = getSerialNumberForUser(user);
mIconDb.delete(
@@ -225,6 +259,7 @@ public abstract class BaseIconCache {
new String[]{packageName + "/%", Long.toString(userSerial)});
}
+ @NonNull
public IconCacheUpdateHandler getUpdateHandler() {
updateSystemState();
return new IconCacheUpdateHandler(this);
@@ -238,12 +273,30 @@ public abstract class BaseIconCache {
private void updateSystemState() {
mLocaleList = mContext.getResources().getConfiguration().getLocales();
mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.SDK_INT;
+ mUserFormatString.clear();
}
- protected String getIconSystemState(String packageName) {
+ @NonNull
+ protected String getIconSystemState(@Nullable final String packageName) {
return mSystemState;
}
+ public CharSequence getUserBadgedLabel(CharSequence label, UserHandle user) {
+ int key = user.hashCode();
+ int index = mUserFormatString.indexOfKey(key);
+ String format;
+ if (index < 0) {
+ format = mPackageManager.getUserBadgedLabel(IDENTITY_FORMAT_STRING, user).toString();
+ if (TextUtils.equals(IDENTITY_FORMAT_STRING, format)) {
+ format = null;
+ }
+ mUserFormatString.put(key, format);
+ } else {
+ format = mUserFormatString.valueAt(index);
+ }
+ return format == null ? label : String.format(format, label);
+ }
+
/**
* Adds an entry into the DB and the in-memory cache.
* @param replaceExisting if true, it will recreate the bitmap even if it already exists in
@@ -251,8 +304,9 @@ public abstract class BaseIconCache {
* old data.
*/
@VisibleForTesting
- public synchronized <T> void addIconToDBAndMemCache(T object, CachingLogic<T> cachingLogic,
- PackageInfo info, long userSerial, boolean replaceExisting) {
+ public synchronized <T> void addIconToDBAndMemCache(@NonNull final T object,
+ @NonNull final CachingLogic<T> cachingLogic, @NonNull final PackageInfo info,
+ final long userSerial, final boolean replaceExisting) {
UserHandle user = cachingLogic.getUser(object);
ComponentName componentName = cachingLogic.getComponent(object);
@@ -276,11 +330,12 @@ public abstract class BaseIconCache {
CharSequence entryTitle = cachingLogic.getLabel(object);
if (entryTitle == null) {
- Log.d(TAG, "No label returned from caching logic instance: " + cachingLogic);
+ Log.wtf(TAG, "No label returned from caching logic instance: " + cachingLogic);
+ entryTitle = "";
}
entry.title = entryTitle;
- entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ entry.contentDescription = getUserBadgedLabel(entry.title, user);
if (cachingLogic.addToMemCache()) mCache.put(key, entry);
ContentValues values = newContentValues(entry.bitmap, entry.title.toString(),
@@ -293,8 +348,8 @@ public abstract class BaseIconCache {
* Updates {@param values} to contain versioning information and adds it to the DB.
* @param values {@link ContentValues} containing icon & title
*/
- private void addIconToDB(ContentValues values, ComponentName key,
- PackageInfo info, long userSerial, long lastUpdateTime) {
+ private void addIconToDB(@NonNull final ContentValues values, @NonNull final ComponentName key,
+ @NonNull final PackageInfo info, final long userSerial, final long lastUpdateTime) {
values.put(IconDB.COLUMN_COMPONENT, key.flattenToString());
values.put(IconDB.COLUMN_USER, userSerial);
values.put(IconDB.COLUMN_LAST_UPDATED, lastUpdateTime);
@@ -302,7 +357,8 @@ public abstract class BaseIconCache {
mIconDb.insertOrReplace(values);
}
- public synchronized BitmapInfo getDefaultIcon(UserHandle user) {
+ @NonNull
+ public synchronized BitmapInfo getDefaultIcon(@NonNull final UserHandle user) {
if (mDefaultIcon == null) {
try (BaseIconFactory li = getIconFactory()) {
mDefaultIcon = li.makeDefaultIcon();
@@ -311,7 +367,8 @@ public abstract class BaseIconCache {
return mDefaultIcon.withFlags(getUserFlagOpLocked(user));
}
- protected FlagOp getUserFlagOpLocked(UserHandle user) {
+ @NonNull
+ protected FlagOp getUserFlagOpLocked(@NonNull final UserHandle user) {
int key = user.hashCode();
int index;
if ((index = mUserFlagOpMap.indexOfKey(key)) >= 0) {
@@ -325,7 +382,7 @@ public abstract class BaseIconCache {
}
}
- public boolean isDefaultIcon(BitmapInfo icon, UserHandle user) {
+ public boolean isDefaultIcon(@NonNull final BitmapInfo icon, @NonNull final UserHandle user) {
return getDefaultIcon(user).icon == icon.icon;
}
@@ -333,10 +390,11 @@ public abstract class BaseIconCache {
* Retrieves the entry from the cache. If the entry is not present, it creates a new entry.
* This method is not thread safe, it must be called from a synchronized method.
*/
+ @NonNull
protected <T> CacheEntry cacheLocked(
- @NonNull ComponentName componentName, @NonNull UserHandle user,
- @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
- boolean usePackageIcon, boolean useLowResIcon) {
+ @NonNull final ComponentName componentName, @NonNull final UserHandle user,
+ @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
+ final boolean usePackageIcon, final boolean useLowResIcon) {
return cacheLocked(
componentName,
user,
@@ -347,10 +405,12 @@ public abstract class BaseIconCache {
useLowResIcon);
}
+ @NonNull
protected <T> CacheEntry cacheLocked(
- @NonNull ComponentName componentName, @NonNull UserHandle user,
- @NonNull Supplier<T> infoProvider, @NonNull CachingLogic<T> cachingLogic,
- @Nullable Cursor cursor, boolean usePackageIcon, boolean useLowResIcon) {
+ @NonNull final ComponentName componentName, @NonNull final UserHandle user,
+ @NonNull final Supplier<T> infoProvider, @NonNull final CachingLogic<T> cachingLogic,
+ @Nullable final Cursor cursor, final boolean usePackageIcon,
+ final boolean useLowResIcon) {
assertWorkerThread();
ComponentKey cacheKey = new ComponentKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);
@@ -396,30 +456,28 @@ public abstract class BaseIconCache {
/**
* Fallback method for loading an icon bitmap.
*/
- protected <T> void loadFallbackIcon(
- T object, CacheEntry entry, @NonNull CachingLogic<T> cachingLogic,
- boolean usePackageIcon, boolean usePackageTitle, @NonNull ComponentName componentName,
- @NonNull UserHandle user) {
+ protected <T> void loadFallbackIcon(@Nullable final T object, @NonNull final CacheEntry entry,
+ @NonNull final CachingLogic<T> cachingLogic, final boolean usePackageIcon,
+ final boolean usePackageTitle, @NonNull final ComponentName componentName,
+ @NonNull final UserHandle user) {
if (object != null) {
entry.bitmap = cachingLogic.loadIcon(mContext, object);
} else {
if (usePackageIcon) {
CacheEntry packageEntry = getEntryForPackageLocked(
componentName.getPackageName(), user, false);
- if (packageEntry != null) {
- if (DEBUG) Log.d(TAG, "using package default icon for " +
- componentName.toShortString());
- entry.bitmap = packageEntry.bitmap;
- entry.contentDescription = packageEntry.contentDescription;
-
- if (usePackageTitle) {
- entry.title = packageEntry.title;
- }
+ if (DEBUG) Log.d(TAG, "using package default icon for " +
+ componentName.toShortString());
+ entry.bitmap = packageEntry.bitmap;
+ entry.contentDescription = packageEntry.contentDescription;
+
+ if (usePackageTitle) {
+ entry.title = packageEntry.title;
}
}
if (entry.bitmap == null) {
- if (DEBUG) Log.d(TAG, "using default icon for " +
- componentName.toShortString());
+ // TODO: entry.bitmap can never be null, so this should not happen at all.
+ Log.wtf(TAG, "using default icon for " + componentName.toShortString());
entry.bitmap = getDefaultIcon(user);
}
}
@@ -429,10 +487,10 @@ public abstract class BaseIconCache {
* Fallback method for loading an app title.
*/
protected <T> void loadFallbackTitle(
- T object, CacheEntry entry, @NonNull CachingLogic<T> cachingLogic,
- @NonNull UserHandle user) {
+ @NonNull final T object, @NonNull final CacheEntry entry,
+ @NonNull final CachingLogic<T> cachingLogic, @NonNull final UserHandle user) {
entry.title = cachingLogic.getLabel(object);
- entry.contentDescription = mPackageManager.getUserBadgedLabel(
+ entry.contentDescription = getUserBadgedLabel(
cachingLogic.getDescription(object, entry.title), user);
}
@@ -445,8 +503,9 @@ public abstract class BaseIconCache {
* Adds a default package entry in the cache. This entry is not persisted and will be removed
* when the cache is flushed.
*/
- protected synchronized void cachePackageInstallInfo(String packageName, UserHandle user,
- Bitmap icon, CharSequence title) {
+ protected synchronized void cachePackageInstallInfo(@NonNull final String packageName,
+ @NonNull final UserHandle user, @Nullable final Bitmap icon,
+ @Nullable final CharSequence title) {
removeFromMemCacheLocked(packageName, user);
ComponentKey cacheKey = getPackageKey(packageName, user);
@@ -469,7 +528,9 @@ public abstract class BaseIconCache {
}
}
- private static ComponentKey getPackageKey(String packageName, UserHandle user) {
+ @NonNull
+ private static ComponentKey getPackageKey(@NonNull final String packageName,
+ @NonNull final UserHandle user) {
ComponentName cn = new ComponentName(packageName, packageName + EMPTY_CLASS_NAME);
return new ComponentKey(cn, user);
}
@@ -478,8 +539,10 @@ public abstract class BaseIconCache {
* Gets an entry for the package, which can be used as a fallback entry for various components.
* This method is not thread safe, it must be called from a synchronized method.
*/
- protected CacheEntry getEntryForPackageLocked(String packageName, UserHandle user,
- boolean useLowResIcon) {
+ @WorkerThread
+ @NonNull
+ protected CacheEntry getEntryForPackageLocked(@NonNull final String packageName,
+ @NonNull final UserHandle user, final boolean useLowResIcon) {
assertWorkerThread();
ComponentKey cacheKey = getPackageKey(packageName, user);
CacheEntry entry = mCache.get(cacheKey);
@@ -508,7 +571,7 @@ public abstract class BaseIconCache {
li.close();
entry.title = appInfo.loadLabel(mPackageManager);
- entry.contentDescription = mPackageManager.getUserBadgedLabel(entry.title, user);
+ entry.contentDescription = getUserBadgedLabel(entry.title, user);
entry.bitmap = BitmapInfo.of(
useLowResIcon ? LOW_RES_ICON : iconInfo.icon, iconInfo.color);
@@ -533,8 +596,8 @@ public abstract class BaseIconCache {
return entry;
}
- protected boolean getEntryFromDBLocked(
- ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
+ protected boolean getEntryFromDBLocked(@NonNull final ComponentKey cacheKey,
+ @NonNull final CacheEntry entry, final boolean lowRes) {
Cursor c = null;
Trace.beginSection("loadIconIndividually");
try {
@@ -559,7 +622,8 @@ public abstract class BaseIconCache {
}
private boolean updateTitleAndIconLocked(
- ComponentKey cacheKey, CacheEntry entry, Cursor c, boolean lowRes) {
+ @NonNull final ComponentKey cacheKey, @NonNull final CacheEntry entry,
+ @NonNull final Cursor c, final boolean lowRes) {
// Set the alpha to be 255, so that we never have a wrong color
entry.bitmap = BitmapInfo.of(LOW_RES_ICON,
setColorAlphaBound(c.getInt(IconDB.INDEX_COLOR), 255));
@@ -568,8 +632,7 @@ public abstract class BaseIconCache {
entry.title = "";
entry.contentDescription = "";
} else {
- entry.contentDescription = mPackageManager.getUserBadgedLabel(
- entry.title, cacheKey.user);
+ entry.contentDescription = getUserBadgedLabel(entry.title, cacheKey.user);
}
if (!lowRes) {
@@ -678,8 +741,10 @@ public abstract class BaseIconCache {
}
}
- private ContentValues newContentValues(BitmapInfo bitmapInfo, String label,
- String packageName, @Nullable String keywords) {
+ @NonNull
+ private ContentValues newContentValues(@NonNull final BitmapInfo bitmapInfo,
+ @NonNull final String label, @NonNull final String packageName,
+ @Nullable final String keywords) {
ContentValues values = new ContentValues();
if (bitmapInfo.canPersist()) {
values.put(IconDB.COLUMN_ICON, flattenBitmap(bitmapInfo.icon));
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
index c12e9dc..8034d6e 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/CachingLogic.java
@@ -28,31 +28,36 @@ import com.android.launcher3.icons.BitmapInfo;
public interface CachingLogic<T> {
- ComponentName getComponent(T object);
+ @NonNull
+ ComponentName getComponent(@NonNull final T object);
- UserHandle getUser(T object);
+ @NonNull
+ UserHandle getUser(@NonNull final T object);
- CharSequence getLabel(T object);
+ @NonNull
+ CharSequence getLabel(@NonNull final T object);
- default CharSequence getDescription(T object, CharSequence fallback) {
+ @NonNull
+ default CharSequence getDescription(@NonNull final T object,
+ @NonNull final CharSequence fallback) {
return fallback;
}
@NonNull
- BitmapInfo loadIcon(Context context, T object);
+ BitmapInfo loadIcon(@NonNull final Context context, @NonNull final T object);
/**
* Provides a option list of keywords to associate with this object
*/
@Nullable
- default String getKeywords(T object, LocaleList localeList) {
+ default String getKeywords(@NonNull final T object, @NonNull final LocaleList localeList) {
return null;
}
/**
* Returns the timestamp the entry was last updated in cache.
*/
- default long getLastUpdatedTime(T object, PackageInfo info) {
+ default long getLastUpdatedTime(@Nullable final T object, @NonNull final PackageInfo info) {
return info.lastUpdateTime;
}
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
index 9e1ad7b..aec1cdd 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/cache/IconCacheUpdateHandler.java
@@ -180,7 +180,8 @@ public class IconCacheUpdateHandler {
long updateTime = c.getLong(indexLastUpdate);
int version = c.getInt(indexVersion);
T app = componentMap.remove(component);
- if (version == info.versionCode && updateTime == info.lastUpdateTime
+ if (version == info.versionCode
+ && updateTime == cachingLogic.getLastUpdatedTime(app, info)
&& TextUtils.equals(c.getString(systemStateIndex),
mIconCache.getIconSystemState(info.packageName))) {
diff --git a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
index cc4ad7b..63ba887 100644
--- a/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
+++ b/iconloaderlib/src_full_lib/com/android/launcher3/icons/SimpleIconCache.java
@@ -32,6 +32,8 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.SparseLongArray;
+import androidx.annotation.NonNull;
+
import com.android.launcher3.icons.cache.BaseIconCache;
/**
@@ -64,7 +66,7 @@ public class SimpleIconCache extends BaseIconCache {
}
@Override
- protected long getSerialNumberForUser(UserHandle user) {
+ protected long getSerialNumberForUser(@NonNull UserHandle user) {
synchronized (mUserSerialMap) {
int index = mUserSerialMap.indexOfKey(user.getIdentifier());
if (index >= 0) {
@@ -83,10 +85,11 @@ public class SimpleIconCache extends BaseIconCache {
}
@Override
- protected boolean isInstantApp(ApplicationInfo info) {
+ protected boolean isInstantApp(@NonNull ApplicationInfo info) {
return info.isInstantApp();
}
+ @NonNull
@Override
public BaseIconFactory getIconFactory() {
return IconFactory.obtain(mContext);
diff --git a/searchuilib/.gitignore b/motiontoollib/.gitignore
index 6213826..6213826 100644
--- a/searchuilib/.gitignore
+++ b/motiontoollib/.gitignore
diff --git a/motiontoollib/Android.bp b/motiontoollib/Android.bp
new file mode 100644
index 0000000..6762d83
--- /dev/null
+++ b/motiontoollib/Android.bp
@@ -0,0 +1,80 @@
+// Copyright (C) 2022 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"],
+}
+
+java_library {
+ name: "motion_tool_proto",
+ srcs: ["src/com/android/app/motiontool/proto/*.proto"],
+ proto: {
+ type: "lite",
+ local_include_dirs:[
+ "src/com/android/app/motiontool/proto"
+ ],
+ include_dirs: [
+ "frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto"
+ ],
+ },
+ static_libs: [
+ "libprotobuf-java-lite",
+ "view_capture_proto",
+ ],
+ java_version: "1.8",
+}
+
+android_library {
+ name: "motion_tool_lib",
+ manifest: "AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture",
+ "motion_tool_proto",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt"
+ ],
+}
+
+android_test {
+ name: "motion_tool_lib_tests",
+ manifest: "tests/AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture",
+ "motion_tool_proto",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "testables"
+ ],
+ srcs: [
+ "**/*.java",
+ "**/*.kt"
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ ],
+ test_suites: ["device-tests"],
+}
+
diff --git a/motiontoollib/AndroidManifest.xml b/motiontoollib/AndroidManifest.xml
new file mode 100644
index 0000000..3b8a656
--- /dev/null
+++ b/motiontoollib/AndroidManifest.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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.motiontool">
+</manifest>
diff --git a/motiontoollib/OWNERS b/motiontoollib/OWNERS
new file mode 100644
index 0000000..fd22ee7
--- /dev/null
+++ b/motiontoollib/OWNERS
@@ -0,0 +1,3 @@
+gallmann@google.com
+michschn@google.com
+cinek@google.com \ No newline at end of file
diff --git a/motiontoollib/TEST_MAPPING b/motiontoollib/TEST_MAPPING
new file mode 100644
index 0000000..22a9e6b
--- /dev/null
+++ b/motiontoollib/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "presubmit": [
+ {
+ "name": "motion_tool_lib_tests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/motiontoollib/build.gradle b/motiontoollib/build.gradle
new file mode 100644
index 0000000..e3750ec
--- /dev/null
+++ b/motiontoollib/build.gradle
@@ -0,0 +1,63 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'com.google.protobuf'
+}
+
+final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/motiontoollib/src/com/android/app/motiontool/proto"
+
+android {
+ namespace = "com.android.app.motiontool"
+ testNamespace = "com.android.app.motiontool.tests"
+ defaultConfig {
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ proto.srcDirs = ["${PROTOS_DIR}"]
+ }
+ androidTest {
+ java.srcDirs = ["tests"]
+ manifest.srcFile "tests/AndroidManifest.xml"
+ }
+ }
+ lint {
+ abortOnError false
+ }
+
+}
+
+dependencies {
+ implementation "androidx.core:core:1.9.0"
+ implementation "com.google.protobuf:protobuf-lite:${protobuf_lite_version}"
+ api project(":ViewCaptureLib")
+ androidTestImplementation project(':SharedTestLib')
+ androidTestImplementation 'androidx.test.ext:junit:1.1.3'
+ androidTestImplementation "androidx.test:rules:1.4.0"
+}
+
+protobuf {
+ // Configure the protoc executable
+ protoc {
+ artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+ }
+ plugins {
+ javalite {
+ // The codegen for lite comes as a separate artifact
+ artifact = "com.google.protobuf:protoc-gen-javalite:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+ }
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ remove java
+ }
+ task.plugins {
+ javalite { }
+ }
+ }
+ }
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
new file mode 100644
index 0000000..c7a6b0d
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2022 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.motiontool
+
+import android.ddm.DdmHandle
+import com.google.protobuf.InvalidProtocolBufferException
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler
+import org.apache.harmony.dalvik.ddmc.DdmServer
+
+/**
+ * This class handles the 'MOTO' type DDM requests (defined in [motion_tool.proto]).
+ *
+ * It executes some validity checks and forwards valid requests to the [MotionToolManager]. It
+ * requires a [MotionToolsRequest] as parameter and returns a [MotionToolsResponse]. Failures will
+ * return a [MotionToolsResponse] with the [error][MotionToolsResponse.error] field set instead of
+ * the respective return value.
+ *
+ * To activate this server, call [register]. This will register the DdmHandleMotionTool with the
+ * [DdmServer]. The DdmHandleMotionTool can be registered once per process. To unregister from the
+ * DdmServer, call [unregister].
+ */
+class DdmHandleMotionTool private constructor(
+ private val motionToolManager: MotionToolManager
+) : DdmHandle() {
+
+ companion object {
+ val CHUNK_MOTO = ChunkHandler.type("MOTO")
+ private const val SERVER_VERSION = 1
+
+ private var INSTANCE: DdmHandleMotionTool? = null
+
+ @Synchronized
+ fun getInstance(motionToolManager: MotionToolManager): DdmHandleMotionTool {
+ return INSTANCE ?: DdmHandleMotionTool(motionToolManager).also {
+ INSTANCE = it
+ }
+ }
+ }
+
+ fun register() {
+ DdmServer.registerHandler(CHUNK_MOTO, this)
+ }
+
+ fun unregister() {
+ DdmServer.unregisterHandler(CHUNK_MOTO)
+ }
+
+ override fun handleChunk(request: Chunk): Chunk {
+ val requestDataBuffer = wrapChunk(request)
+ val protoRequest =
+ try {
+ MotionToolsRequest.parseFrom(requestDataBuffer.array())
+ } catch (e: InvalidProtocolBufferException) {
+ val responseData: ByteArray = MotionToolsResponse.newBuilder()
+ .setError(ErrorResponse.newBuilder()
+ .setCode(ErrorResponse.Code.INVALID_REQUEST)
+ .setMessage("Invalid request format (Protobuf parse exception)"))
+ .build()
+ .toByteArray()
+ return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+ }
+
+ val response =
+ when (protoRequest.typeCase.number) {
+ MotionToolsRequest.HANDSHAKE_FIELD_NUMBER ->
+ handleHandshakeRequest(protoRequest.handshake)
+ MotionToolsRequest.BEGIN_TRACE_FIELD_NUMBER ->
+ handleBeginTraceRequest(protoRequest.beginTrace)
+ MotionToolsRequest.POLL_TRACE_FIELD_NUMBER ->
+ handlePollTraceRequest(protoRequest.pollTrace)
+ MotionToolsRequest.END_TRACE_FIELD_NUMBER ->
+ handleEndTraceRequest(protoRequest.endTrace)
+ else ->
+ MotionToolsResponse.newBuilder().setError(ErrorResponse.newBuilder()
+ .setCode(ErrorResponse.Code.INVALID_REQUEST)
+ .setMessage("Unknown request type")).build()
+ }
+
+ val responseData = response.toByteArray()
+ return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
+ }
+
+ private fun handleBeginTraceRequest(beginTraceRequest: BeginTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
+ tryCatchingMotionToolManagerExceptions {
+ setBeginTrace(BeginTraceResponse.newBuilder().setTraceId(
+ motionToolManager.beginTrace(beginTraceRequest.window.rootWindow)))
+ }
+ }.build()
+
+ private fun handlePollTraceRequest(pollTraceRequest: PollTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
+ tryCatchingMotionToolManagerExceptions {
+ setPollTrace(PollTraceResponse.newBuilder()
+ .setData(motionToolManager.pollTrace(pollTraceRequest.traceId)))
+ }
+ }.build()
+
+ private fun handleEndTraceRequest(endTraceRequest: EndTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
+ tryCatchingMotionToolManagerExceptions {
+ setEndTrace(EndTraceResponse.newBuilder()
+ .setData(motionToolManager.endTrace(endTraceRequest.traceId)))
+ }
+ }.build()
+
+ private fun handleHandshakeRequest(handshakeRequest: HandshakeRequest): MotionToolsResponse {
+ val status = if (motionToolManager.hasWindow(handshakeRequest.window))
+ HandshakeResponse.Status.OK
+ else
+ HandshakeResponse.Status.WINDOW_NOT_FOUND
+
+ return MotionToolsResponse.newBuilder()
+ .setHandshake(HandshakeResponse.newBuilder()
+ .setServerVersion(SERVER_VERSION)
+ .setStatus(status))
+ .build()
+ }
+
+ /**
+ * Executes the [block] and catches all Exceptions thrown by [MotionToolManager]. In case of an
+ * exception being caught, the error response field of the [MotionToolsResponse] is being set
+ * with the according [ErrorResponse].
+ */
+ private fun MotionToolsResponse.Builder.tryCatchingMotionToolManagerExceptions(block: () -> Unit) {
+ try {
+ block()
+ } catch (e: UnknownTraceIdException) {
+ setError(createUnknownTraceIdResponse(e.traceId))
+ } catch (e: WindowNotFoundException) {
+ setError(createWindowNotFoundResponse(e.windowId))
+ }
+ }
+
+ private fun createUnknownTraceIdResponse(traceId: Int) =
+ ErrorResponse.newBuilder().apply {
+ this.code = ErrorResponse.Code.UNKNOWN_TRACE_ID
+ this.message = "No running Trace found with traceId $traceId"
+ }
+
+ private fun createWindowNotFoundResponse(windowId: String) =
+ ErrorResponse.newBuilder().apply {
+ this.code = ErrorResponse.Code.WINDOW_NOT_FOUND
+ this.message = "No window found with windowId $windowId"
+ }
+
+ override fun onConnected() {}
+
+ override fun onDisconnected() {}
+}
diff --git a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
new file mode 100644
index 0000000..a98a588
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2022 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.motiontool
+
+import android.os.Process
+import android.util.Log
+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.MotionWindowData
+
+/**
+ * Singleton to manage motion tracing sessions.
+ *
+ * A motion tracing session captures motion-relevant data on a frame-by-frame basis for a given
+ * window, as long as the trace is running.
+ *
+ * To start a trace, use [beginTrace]. The returned handle must be used to terminate tracing and
+ * receive the data by calling [endTrace]. While the trace is active, data is buffered, however
+ * the buffer size is limited (@see [ViewCapture.mMemorySize]. Use [pollTrace] periodically to
+ * ensure no data is dropped. Both, [pollTrace] and [endTrace] only return data captured since the
+ * last call to either [beginTrace] or [endTrace].
+ *
+ * NOTE: a running trace will incur some performance penalty. Only keep traces running while a user
+ * requested it.
+ *
+ * @see [DdmHandleMotionTool]
+ */
+class MotionToolManager private constructor(private val windowManagerGlobal: WindowManagerGlobal) {
+ private val viewCapture: ViewCapture = SimpleViewCapture("MTViewCapture")
+
+ companion object {
+ private const val TAG = "MotionToolManager"
+
+ private var INSTANCE: MotionToolManager? = null
+
+ @Synchronized
+ fun getInstance(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+ return INSTANCE ?: MotionToolManager(windowManagerGlobal).also { INSTANCE = it }
+ }
+ }
+
+ private var traceIdCounter = 0
+ private val traces = mutableMapOf<Int, TraceMetadata>()
+
+ @Synchronized
+ fun hasWindow(windowId: WindowIdentifier): Boolean {
+ val rootView = getRootView(windowId.rootWindow)
+ return rootView != null
+ }
+
+ /** Starts [ViewCapture] and returns a traceId. */
+ @Synchronized
+ fun beginTrace(windowId: String): Int {
+ val traceId = ++traceIdCounter
+ Log.d(TAG, "Begin Trace for id: $traceId")
+ val rootView = getRootView(windowId) ?: throw WindowNotFoundException(windowId)
+ val autoCloseable = viewCapture.startCapture(rootView, windowId)
+ traces[traceId] = TraceMetadata(windowId, 0, autoCloseable::close)
+ return traceId
+ }
+
+ /**
+ * Ends [ViewCapture] and returns the captured [MotionWindowData] since the [beginTrace] call or
+ * the last [pollTrace] call.
+ */
+ @Synchronized
+ fun endTrace(traceId: Int): MotionWindowData {
+ Log.d(TAG, "End Trace for id: $traceId")
+ val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+ val data = pollTrace(traceId)
+ traceMetadata.stopTrace()
+ traces.remove(traceId)
+ return data
+ }
+
+ /**
+ * 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): MotionWindowData {
+ val traceMetadata = traces.getOrElse(traceId) { throw UnknownTraceIdException(traceId) }
+ val data = getDataFromViewCapture(traceMetadata)
+ traceMetadata.updateLastPolledTime(data)
+ return data
+ }
+
+ /**
+ * Stops and deletes all active [traces] and resets the [traceIdCounter].
+ */
+ @VisibleForTesting
+ @Synchronized
+ fun reset() {
+ for (traceMetadata in traces.values) {
+ traceMetadata.stopTrace()
+ }
+ traces.clear()
+ traceIdCounter = 0
+ }
+
+ private fun getDataFromViewCapture(traceMetadata: TraceMetadata): MotionWindowData {
+ val rootView =
+ getRootView(traceMetadata.windowId)
+ ?: throw WindowNotFoundException(traceMetadata.windowId)
+
+ 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)
+ }
+}
+
+private data class TraceMetadata(
+ val windowId: String,
+ var lastPolledTime: Long,
+ var stopTrace: () -> Unit
+) {
+ fun updateLastPolledTime(data: MotionWindowData?) {
+ data?.frameDataList?.maxOfOrNull { it.timestamp }?.let {
+ lastPolledTime = it
+ }
+ }
+}
+
+class UnknownTraceIdException(val traceId: Int) : Exception()
+
+class WindowNotFoundException(val windowId: String) : Exception() \ No newline at end of file
diff --git a/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
new file mode 100644
index 0000000..04ee020
--- /dev/null
+++ b/motiontoollib/src/com/android/app/motiontool/proto/motion_tool.proto
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto2";
+
+package com.android.app.motiontool;
+
+import "view_capture.proto";
+
+option java_multiple_files = true;
+
+message MotionToolsRequest {
+ oneof type {
+ HandshakeRequest handshake = 1;
+ BeginTraceRequest begin_trace = 2;
+ EndTraceRequest end_trace = 3;
+ PollTraceRequest poll_trace = 4;
+ }
+}
+
+// RPC response messages.
+//
+// Returns the result from the corresponding request.
+message MotionToolsResponse {
+ oneof type {
+ // Contains error information whenever the request failed.
+ ErrorResponse error = 1;
+
+ HandshakeResponse handshake = 2;
+ BeginTraceResponse begin_trace = 3;
+ EndTraceResponse end_trace = 4;
+ PollTraceResponse poll_trace = 5;
+ }
+}
+
+message ErrorResponse {
+ enum Code {
+ UNKNOWN = 0;
+ INVALID_REQUEST = 1;
+ UNKNOWN_TRACE_ID = 2;
+ WINDOW_NOT_FOUND = 3;
+ }
+
+ optional Code code = 1;
+ // Human readable error message.
+ optional string message = 2;
+}
+
+// Identifies the window, in which context the motion tools are executed
+message WindowIdentifier {
+ // An identifier for the root view, as accepted by
+ // WindowManagerGlobal#getRootView. This is formatted as
+ // `windowName/rootViewClassName@rootViewIdentityHashCode`,
+ // for example `NotificationShade/android.view.ViewRootImpl@bab6a53`.
+ optional string root_window = 1;
+}
+
+// Verifies the motion tools are available for the specified window.
+message HandshakeRequest {
+ optional WindowIdentifier window = 1;
+ optional int32 client_version = 2;
+}
+
+message HandshakeResponse {
+ enum Status {
+ OK = 1;
+ WINDOW_NOT_FOUND = 2;
+ }
+ optional Status status = 1;
+ optional int32 server_version = 2;
+}
+
+// Enables motion tracing for the specified window
+message BeginTraceRequest {
+ optional WindowIdentifier window = 1;
+}
+
+message BeginTraceResponse {
+ optional int32 trace_id = 1;
+}
+
+// Disabled motion tracing for the specified window
+message EndTraceRequest {
+ optional int32 trace_id = 1;
+}
+
+message EndTraceResponse {
+ optional com.android.app.viewcapture.data.MotionWindowData data = 1;
+}
+
+// Polls collected motion trace data collected since the last PollTraceRequest (or the
+// BeginTraceRequest)
+message PollTraceRequest {
+ optional int32 trace_id = 1;
+}
+
+message PollTraceResponse {
+ optional com.android.app.viewcapture.data.MotionWindowData data = 1;
+}
+
diff --git a/motiontoollib/tests/AndroidManifest.xml b/motiontoollib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..c16e25f
--- /dev/null
+++ b/motiontoollib/tests/AndroidManifest.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2022 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.motiontool.tests">
+
+ <application
+ android:debuggable="true"
+ android:theme="@android:style/Theme.NoTitleBar">
+
+ <activity
+ android:name="com.android.app.motiontool.util.TestActivity"
+ android:exported="false" />
+
+ <uses-library android:name="android.test.runner" />
+
+ </application>
+
+ <instrumentation
+ android:name="android.testing.TestableInstrumentation"
+ android:label="Tests for MotionTool Lib"
+ 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
new file mode 100644
index 0000000..f330980
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2022 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.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.DdmHandleMotionTool.Companion.CHUNK_MOTO
+import com.android.app.motiontool.util.TestActivity
+import junit.framework.Assert
+import junit.framework.Assert.assertEquals
+import org.apache.harmony.dalvik.ddmc.Chunk
+import org.apache.harmony.dalvik.ddmc.ChunkHandler.wrapChunk
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class DdmHandleMotionToolTest {
+
+ private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+ private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
+ private val ddmHandleMotionTool = DdmHandleMotionTool.getInstance(motionToolManager)
+ private val CLIENT_VERSION = 1
+
+ private val activityIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @Before
+ fun setup() {
+ ddmHandleMotionTool.register()
+ }
+
+ @After
+ fun cleanup() {
+ ddmHandleMotionTool.unregister()
+ }
+
+ @Test
+ fun testHandshakeErrorWithInvalidWindowId() {
+ val handshakeResponse = performHandshakeRequest("InvalidWindowId")
+ assertEquals(HandshakeResponse.Status.WINDOW_NOT_FOUND, handshakeResponse.handshake.status)
+ }
+
+ @Test
+ fun testHandshakeOkWithValidWindowId() {
+ val handshakeResponse = performHandshakeRequest(getActivityViewRootId())
+ assertEquals(HandshakeResponse.Status.OK, handshakeResponse.handshake.status)
+ }
+
+ @Test
+ fun testBeginFailsWithInvalidWindowId() {
+ val errorResponse = performBeginTraceRequest("InvalidWindowId")
+ assertEquals(ErrorResponse.Code.WINDOW_NOT_FOUND, errorResponse.error.code)
+ }
+
+ @Test
+ fun testEndTraceFailsWithoutPrecedingBeginTrace() {
+ val errorResponse = performEndTraceRequest(0)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ }
+
+ @Test
+ fun testPollTraceFailsWithoutPrecedingBeginTrace() {
+ val errorResponse = performPollTraceRequest(0)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ }
+
+ @Test
+ fun testEndTraceFailsWithInvalidTraceId() {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ }
+
+ @Test
+ fun testPollTraceFailsWithInvalidTraceId() {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performPollTraceRequest(beginTraceResponse.beginTrace.traceId + 1)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ }
+
+ @Test
+ fun testMalformedRequestFails() {
+ val requestBytes = ByteArray(9)
+ val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+ val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+ val response = MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array()).error
+ assertEquals(ErrorResponse.Code.INVALID_REQUEST, response.code)
+ }
+
+ @Test
+ fun testNoOnDrawCallReturnsEmptyTrace() {
+ activityScenarioRule.scenario.onActivity {
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId)
+ Assert.assertTrue(endTraceResponse.endTrace.data.frameDataList.isEmpty())
+ }
+ }
+
+ @Test
+ fun testOneOnDrawCallReturnsOneFrameResponse() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
+ val traceId = beginTraceResponse.beginTrace.traceId
+
+ Choreographer.getInstance().postFrameCallback {
+ activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
+
+ val pollTraceResponse = performPollTraceRequest(traceId)
+ 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.data.frameDataList.size)
+ }
+ }
+ }
+
+ private fun performPollTraceRequest(requestTraceId: Int): MotionToolsResponse {
+ val pollTraceRequest = MotionToolsRequest.newBuilder()
+ .setPollTrace(PollTraceRequest.newBuilder()
+ .setTraceId(requestTraceId))
+ .build()
+ return performRequest(pollTraceRequest)
+ }
+
+ private fun performEndTraceRequest(requestTraceId: Int): MotionToolsResponse {
+ val endTraceRequest = MotionToolsRequest.newBuilder()
+ .setEndTrace(EndTraceRequest.newBuilder()
+ .setTraceId(requestTraceId))
+ .build()
+ return performRequest(endTraceRequest)
+ }
+
+ private fun performBeginTraceRequest(windowId: String): MotionToolsResponse {
+ val beginTraceRequest = MotionToolsRequest.newBuilder()
+ .setBeginTrace(BeginTraceRequest.newBuilder()
+ .setWindow(WindowIdentifier.newBuilder()
+ .setRootWindow(windowId)))
+ .build()
+ return performRequest(beginTraceRequest)
+ }
+
+ private fun performHandshakeRequest(windowId: String): MotionToolsResponse {
+ val handshakeRequest = MotionToolsRequest.newBuilder()
+ .setHandshake(HandshakeRequest.newBuilder()
+ .setWindow(WindowIdentifier.newBuilder()
+ .setRootWindow(windowId))
+ .setClientVersion(CLIENT_VERSION))
+ .build()
+ return performRequest(handshakeRequest)
+ }
+
+ private fun performRequest(motionToolsRequest: MotionToolsRequest): MotionToolsResponse {
+ val requestBytes = motionToolsRequest.toByteArray()
+ val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
+ val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
+ return MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array())
+ }
+
+ private fun getActivityViewRootId(): String {
+ var activityViewRootId = ""
+ activityScenarioRule.scenario.onActivity {
+ activityViewRootId = WindowManagerGlobal.getInstance().viewRootNames.first()
+ }
+ return activityViewRootId
+ }
+
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
new file mode 100644
index 0000000..c522d0c
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 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.motiontool
+
+import android.content.Intent
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.app.motiontool.util.TestActivity
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertTrue
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class MotionToolManagerTest {
+
+ private val windowManagerGlobal = WindowManagerGlobal.getInstance()
+ private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
+
+ private val activityIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+ @get:Rule
+ val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testEndTraceThrowsWithoutPrecedingBeginTrace() {
+ motionToolManager.endTrace(0)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testPollTraceThrowsWithoutPrecedingBeginTrace() {
+ motionToolManager.pollTrace(0)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testEndTraceThrowsWithInvalidTraceId() {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ motionToolManager.endTrace(traceId + 1)
+ }
+
+ @Test(expected = UnknownTraceIdException::class)
+ fun testPollTraceThrowsWithInvalidTraceId() {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ motionToolManager.pollTrace(traceId + 1)
+ }
+
+ @Test(expected = WindowNotFoundException::class)
+ fun testBeginTraceThrowsWithInvalidWindowId() {
+ motionToolManager.beginTrace("InvalidWindowId")
+ }
+
+ @Test
+ fun testNoOnDrawCallReturnsEmptyResponse() {
+ activityScenarioRule.scenario.onActivity {
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ val result = motionToolManager.endTrace(traceId)
+ assertTrue(result.frameDataList.isEmpty())
+ }
+ }
+
+ @Test
+ fun testOneOnDrawCallReturnsOneFrameResponse() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ Choreographer.getInstance().postFrameCallback {
+ activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
+
+ val polledData = motionToolManager.pollTrace(traceId)
+ assertEquals(1, polledData.frameDataList.size)
+
+ // Verify that frameData is only included once and is not returned again
+ val endData = motionToolManager.endTrace(traceId)
+ assertEquals(0, endData.frameDataList.size)
+ }
+ }
+ }
+
+ private fun getActivityViewRootId(): String {
+ var activityViewRootId = ""
+ activityScenarioRule.scenario.onActivity {
+ activityViewRootId = WindowManagerGlobal.getInstance().viewRootNames.first()
+ }
+ return activityViewRootId
+ }
+}
diff --git a/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
new file mode 100644
index 0000000..a9d68ab
--- /dev/null
+++ b/motiontoollib/tests/com/android/app/motiontool/util/TestActivity.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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.motiontool.util
+
+import android.app.Activity
+
+class TestActivity : Activity()
diff --git a/searchuilib/Android.bp b/searchuilib/Android.bp
deleted file mode 100644
index 2b25616..0000000
--- a/searchuilib/Android.bp
+++ /dev/null
@@ -1,28 +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: "current",
- min_sdk_version: "26",
-
- srcs: [
- "src/**/*.java",
- ],
-}
diff --git a/searchuilib/build.gradle b/searchuilib/build.gradle
deleted file mode 100644
index 3bf65fe..0000000
--- a/searchuilib/build.gradle
+++ /dev/null
@@ -1,35 +0,0 @@
-apply plugin: 'com.android.library'
-
-android {
- compileSdkVersion COMPILE_SDK
- buildToolsVersion BUILD_TOOLS_VERSION
-
- defaultConfig {
- minSdkVersion 25
- targetSdkVersion 28
- }
-
- 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:${ANDROID_X_VERSION}"
-}
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 d7c28ab..0000000
--- a/searchuilib/src/com/android/app/search/LayoutType.java
+++ /dev/null
@@ -1,79 +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 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";
-
- // 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 DIVIDER = "divider";
-
- // 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";
-
- // layout for the section header
- public static final String SECTION_HEADER = "section_header";
-}
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 1e5ea12..0000000
--- a/searchuilib/src/com/android/app/search/ResultType.java
+++ /dev/null
@@ -1,50 +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 SCREENSHOT = 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;
-}
diff --git a/viewcapturelib/.gitignore b/viewcapturelib/.gitignore
new file mode 100644
index 0000000..6213826
--- /dev/null
+++ b/viewcapturelib/.gitignore
@@ -0,0 +1,13 @@
+*.iml
+.project
+.classpath
+.project.properties
+gen/
+bin/
+.idea/
+.gradle/
+local.properties
+gradle/
+build/
+gradlew*
+.DS_Store
diff --git a/viewcapturelib/Android.bp b/viewcapturelib/Android.bp
new file mode 100644
index 0000000..33da2dd
--- /dev/null
+++ b/viewcapturelib/Android.bp
@@ -0,0 +1,73 @@
+// Copyright (C) 2022 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"],
+}
+
+java_library {
+ name: "view_capture_proto",
+ srcs: ["src/com/android/app/viewcapture/proto/*.proto"],
+ proto: {
+ type: "lite",
+ local_include_dirs:[
+ "src/com/android/app/viewcapture/proto"
+ ],
+ },
+ static_libs: ["libprotobuf-java-lite"],
+ java_version: "1.8",
+}
+
+android_library {
+ name: "view_capture",
+ manifest: "AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture_proto",
+ ],
+
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt"
+ ],
+}
+
+android_test {
+ name: "view_capture_tests",
+ manifest: "tests/AndroidManifest.xml",
+ platform_apis: true,
+ min_sdk_version: "26",
+
+ static_libs: [
+ "androidx.core_core",
+ "view_capture",
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "testables",
+ "mockito-target-extended-minus-junit4",
+ ],
+ srcs: [
+ "**/*.java",
+ "**/*.kt"
+ ],
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ ],
+ test_suites: ["device-tests"],
+}
diff --git a/viewcapturelib/AndroidManifest.xml b/viewcapturelib/AndroidManifest.xml
new file mode 100644
index 0000000..1da8129
--- /dev/null
+++ b/viewcapturelib/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2022 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"
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.app.viewcapture">
+ <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
+ tools:ignore="ProtectedPermissions" />
+</manifest>
diff --git a/viewcapturelib/OWNERS b/viewcapturelib/OWNERS
new file mode 100644
index 0000000..30bdc84
--- /dev/null
+++ b/viewcapturelib/OWNERS
@@ -0,0 +1,2 @@
+sunnygoyal@google.com
+andonian@google.com
diff --git a/viewcapturelib/README.md b/viewcapturelib/README.md
new file mode 100644
index 0000000..4a6993f
--- /dev/null
+++ b/viewcapturelib/README.md
@@ -0,0 +1,11 @@
+###ViewCapture Library Readme
+
+ViewCapture.java is extremely performance sensitive. Any changes should be carried out with great caution not to hurt performance.
+
+The following measurements should serve as a performance baseline (as of 02.10.2022):
+
+
+The onDraw() function invocation time in WindowListener within ViewCapture is measured with System.nanoTime(). The following scenario was measured:
+
+1. Capturing the notification shade window root view on a freshly rebooted bluejay device (2 notifications present) -> avg. time = 204237ns (0.2ms)
+
diff --git a/viewcapturelib/TEST_MAPPING b/viewcapturelib/TEST_MAPPING
new file mode 100644
index 0000000..ecd3e96
--- /dev/null
+++ b/viewcapturelib/TEST_MAPPING
@@ -0,0 +1,15 @@
+{
+ "presubmit": [
+ {
+ "name": "view_capture_tests",
+ "options": [
+ {
+ "exclude-annotation": "org.junit.Ignore"
+ },
+ {
+ "exclude-annotation": "androidx.test.filters.FlakyTest"
+ }
+ ]
+ }
+ ]
+}
diff --git a/viewcapturelib/build.gradle b/viewcapturelib/build.gradle
new file mode 100644
index 0000000..e5442cc
--- /dev/null
+++ b/viewcapturelib/build.gradle
@@ -0,0 +1,62 @@
+plugins {
+ id 'com.android.library'
+ id 'org.jetbrains.kotlin.android'
+ id 'com.google.protobuf'
+}
+
+final String PROTOS_DIR = "${ANDROID_TOP}/frameworks/libs/systemui/viewcapturelib/src/com/android/app/viewcapture/proto"
+
+android {
+ namespace = "com.android.app.viewcapture"
+ testNamespace = "com.android.app.viewcapture.test"
+ defaultConfig {
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets {
+ main {
+ java.srcDirs = ['src']
+ manifest.srcFile 'AndroidManifest.xml'
+ proto.srcDirs = ["${PROTOS_DIR}"]
+ }
+ androidTest {
+ java.srcDirs = ["tests"]
+ manifest.srcFile "tests/AndroidManifest.xml"
+ }
+ }
+ lint {
+ abortOnError false
+ }
+
+}
+
+dependencies {
+ implementation "androidx.core:core:1.9.0"
+ 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"
+}
+
+protobuf {
+ // Configure the protoc executable
+ protoc {
+ artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+ }
+ plugins {
+ javalite {
+ // The codegen for lite comes as a separate artifact
+ artifact = "com.google.protobuf:protoc-gen-javalite:${protobuf_version}${PROTO_ARCH_SUFFIX}"
+ }
+ }
+ generateProtoTasks {
+ all().each { task ->
+ task.builtins {
+ remove java
+ }
+ task.plugins {
+ javalite { }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
new file mode 100644
index 0000000..e3450f6
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 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.viewcapture;
+
+import android.os.Handler;
+import android.os.Looper;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Future;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.RunnableFuture;
+
+/**
+ * Implementation of {@link Executor} which executes on a provided looper.
+ */
+public class LooperExecutor implements Executor {
+
+ private final Handler mHandler;
+
+ public LooperExecutor(Looper looper) {
+ mHandler = new Handler(looper);
+ }
+
+ @Override
+ public void execute(Runnable runnable) {
+ if (mHandler.getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ mHandler.post(runnable);
+ }
+ }
+
+ /**
+ * @throws RejectedExecutionException {@inheritDoc}
+ * @throws NullPointerException {@inheritDoc}
+ */
+ public <T> Future<T> submit(Callable<T> task) {
+ if (task == null) throw new NullPointerException();
+ RunnableFuture<T> ftask = new FutureTask<T>(task);
+ execute(ftask);
+ return ftask;
+ }
+
+}
diff --git a/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
new file mode 100644
index 0000000..8a3cf1c
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.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
+ * developer tile in the System developer options.
+ */
+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()
+ context.contentResolver.registerContentObserver(
+ Settings.Global.getUriFor(VIEW_CAPTURE_ENABLED),
+ false,
+ object : ContentObserver(Handler()) {
+ override fun onChange(selfChange: Boolean) {
+ enableOrDisableWindowListeners()
+ }
+ })
+ }
+
+ @AnyThread
+ private fun enableOrDisableWindowListeners() {
+ mBgExecutor.execute {
+ val isEnabled = Settings.Global.getInt(context.contentResolver, VIEW_CAPTURE_ENABLED,
+ 0) != 0
+ MAIN_EXECUTOR.execute {
+ enableOrDisableWindowListeners(isEnabled)
+ }
+ val launcherApps = context.getSystemService(LauncherApps::class.java)
+ if (isEnabled) {
+ launcherApps?.registerDumpCallback(mDumpCallback)
+ } else {
+ launcherApps?.unRegisterDumpCallback(mDumpCallback)
+ }
+ }
+ }
+
+ companion object {
+ @VisibleForTesting internal const val VIEW_CAPTURE_ENABLED = "view_capture_enabled"
+
+ private var INSTANCE: ViewCapture? = null
+
+ @JvmStatic
+ fun getInstance(context: Context): ViewCapture = when {
+ INSTANCE != null -> INSTANCE!!
+ Looper.myLooper() == Looper.getMainLooper() -> SettingsAwareViewCapture(
+ context.applicationContext, Choreographer.getInstance(),
+ createAndStartNewLooperExecutor("SAViewCapture",
+ Process.THREAD_PRIORITY_FOREGROUND)).also { INSTANCE = it }
+ else -> try {
+ MAIN_EXECUTOR.submit { getInstance(context) }.get()
+ } catch (e: Exception) {
+ throw e
+ }
+ }
+
+ }
+} \ No newline at end of file
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
new file mode 100644
index 0000000..fb5abd6
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
@@ -0,0 +1,614 @@
+/*
+ * Copyright (C) 2022 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.viewcapture;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.media.permission.SafeCloseable;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Trace;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.view.Choreographer;
+import android.view.View;
+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.IOException;
+import java.io.OutputStream;
+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.function.Consumer;
+import java.util.function.Predicate;
+
+/**
+ * Utility class for capturing view data every frame
+ */
+public abstract class ViewCapture {
+
+ private static final String TAG = "ViewCapture";
+
+ // These flags are copies of two private flags in the View class.
+ private static final int PFLAG_INVALIDATED = 0x80000000;
+ private static final int PFLAG_DIRTY_MASK = 0x00200000;
+
+ // Number of frames to keep in memory
+ private final int mMemorySize;
+ protected static final int DEFAULT_MEMORY_SIZE = 2000;
+ // Initial size of the reference pool. This is at least be 5 * total number of views in
+ // Launcher. This allows the first free frames avoid object allocation during view capture.
+ protected static final int DEFAULT_INIT_POOL_SIZE = 300;
+
+ public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());
+
+ private final List<WindowListener> mListeners = new ArrayList<>();
+
+ protected final Executor mBgExecutor;
+ private final Choreographer mChoreographer;
+
+ // Pool used for capturing view tree on the UI thread.
+ private ViewRef mPool = new ViewRef();
+ private boolean mIsEnabled = true;
+
+ protected ViewCapture(int memorySize, int initPoolSize, Choreographer choreographer,
+ Executor bgExecutor) {
+ mMemorySize = memorySize;
+ mChoreographer = choreographer;
+ mBgExecutor = bgExecutor;
+ mBgExecutor.execute(() -> initPool(initPoolSize));
+ }
+
+ public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
+ HandlerThread thread = new HandlerThread(name, priority);
+ thread.start();
+ return new LooperExecutor(thread.getLooper());
+ }
+
+ @UiThread
+ private void addToPool(ViewRef start, ViewRef end) {
+ end.next = mPool;
+ mPool = start;
+ }
+
+ @WorkerThread
+ private void initPool(int initPoolSize) {
+ ViewRef start = new ViewRef();
+ ViewRef current = start;
+
+ for (int i = 0; i < initPoolSize; i++) {
+ current.next = new ViewRef();
+ current = current.next;
+ }
+
+ ViewRef finalCurrent = current;
+ MAIN_EXECUTOR.execute(() -> addToPool(start, finalCurrent));
+ }
+
+ /**
+ * 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;
+ return startCapture(window.getDecorView(), name);
+ }
+
+ /**
+ * 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);
+ mListeners.add(listener);
+ return () -> {
+ mListeners.remove(listener);
+ listener.detachFromRoot();
+ };
+ }
+
+ /**
+ * 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;
+ mListeners.forEach(WindowListener::detachFromRoot);
+ if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot);
+ }
+
+ @AnyThread
+ public void dumpTo(OutputStream os, Context context)
+ throws InterruptedException, ExecutionException, IOException {
+ if (!mIsEnabled) {
+ return;
+ }
+ ArrayList<Class> classList = new ArrayList<>();
+ ExportedData.newBuilder()
+ .setPackage(context.getPackageName())
+ .addAllWindowData(getWindowData(context, classList, l -> l.mIsActive).get())
+ .addAllClassname(toStringList(classList))
+ .build()
+ .writeTo(os);
+ }
+
+ private static List<String> toStringList(List<Class> classList) {
+ return classList.stream().map(Class::getName).toList();
+ }
+
+ 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()));
+ }
+
+ @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
+ * tree traversed in a local list of nodes, so that this list of nodes can be processed on a
+ * background thread, and prepared for being dumped into a bugreport.
+ *
+ * Since some of the work needs to be done on the main thread after every draw, this piece of
+ * code needs to be hyper optimized. That is why we are recycling ViewRef and ViewPropertyRef
+ * objects and storing the list of nodes as a flat LinkedList, rather than as a tree. This data
+ * structure allows recycling to happen in O(1) time via pointer assignment. Without this
+ * optimization, a lot of time is wasted creating ViewRef objects, or finding ViewRef objects to
+ * recycle.
+ *
+ * Another optimization is to only traverse view nodes on the main thread that have potentially
+ * changed since the last frame was drawn. This can be determined via a combination of private
+ * flags inside the View class.
+ *
+ * Another optimization is to not store or manipulate any string objects on the main thread.
+ * While this might seem trivial, using Strings in any form causes the ViewCapture to hog the
+ * main thread for up to an additional 6-7ms. It must be avoided at all costs.
+ *
+ * Another optimization is to only store the class names of the Views in the view hierarchy one
+ * time. They are then referenced via a classNameIndex value stored in each ViewPropertyRef.
+ *
+ * TODO: b/262585897: If further memory optimization is required, an effective one would be to
+ * only store the changes between frames, rather than the entire node tree for each frame.
+ * The go/web-hv UX already does this, and has reaped significant memory improves because of it.
+ *
+ * TODO: b/262585897: Another memory optimization could be to store all integer, float, and
+ * boolean information via single integer values via the Chinese remainder theorem, or a similar
+ * algorithm, which enables multiple numerical values to be stored inside 1 number. Doing this
+ * would allow each ViewProperty / ViewRef to slim down its memory footprint significantly.
+ *
+ * One important thing to remember is that bugs related to recycling will usually only appear
+ * after at least 2000 frames have been rendered. If that code is changed, the tester can
+ * use hard-coded logs to verify that recycling is happening, and test view capturing at least
+ * ~8000 frames or so to verify the recycling functionality is working properly.
+ */
+ private class WindowListener implements ViewTreeObserver.OnDrawListener {
+
+ @Nullable // Nullable in tests only
+ public View mRoot;
+ public final String name;
+
+ private final ViewRef mViewRef = new ViewRef();
+
+ private int mFrameIndexBg = -1;
+ private boolean mIsFirstFrame = true;
+ private final long[] mFrameTimesNanosBg = new long[mMemorySize];
+ private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];
+
+ private boolean mIsActive = true;
+ private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
+
+ WindowListener(View view, String name) {
+ mRoot = view;
+ this.name = name;
+ }
+
+ /**
+ * Every time onDraw is called, it does the minimal set of work required on the main thread,
+ * i.e. capturing potentially dirty / invalidated views, and then immediately offloads the
+ * rest of the processing work (extracting the captured view properties) to a background
+ * thread via mExecutor.
+ */
+ @Override
+ public void onDraw() {
+ Trace.beginSection("view_capture");
+ captureViewTree(mRoot, mViewRef);
+ ViewRef captured = mViewRef.next;
+ if (captured != null) {
+ captured.callback = mCaptureCallback;
+ captured.choreographerTimeNanos = mChoreographer.getFrameTimeNanos();
+ mBgExecutor.execute(captured);
+ }
+ mIsFirstFrame = false;
+ Trace.endSection();
+ }
+
+ /**
+ * Captures the View property on the background thread, and transfer all the ViewRef objects
+ * back to the pool
+ */
+ @WorkerThread
+ private void captureViewPropertiesBg(ViewRef viewRefStart) {
+ long choreographerTimeNanos = viewRefStart.choreographerTimeNanos;
+ mFrameIndexBg++;
+ if (mFrameIndexBg >= mMemorySize) {
+ mFrameIndexBg = 0;
+ }
+ mFrameTimesNanosBg[mFrameIndexBg] = choreographerTimeNanos;
+
+ ViewPropertyRef recycle = mNodesBg[mFrameIndexBg];
+
+ ViewPropertyRef resultStart = null;
+ ViewPropertyRef resultEnd = null;
+
+ ViewRef viewRefEnd = viewRefStart;
+ while (viewRefEnd != null) {
+ ViewPropertyRef propertyRef = recycle;
+ if (propertyRef == null) {
+ propertyRef = new ViewPropertyRef();
+ } else {
+ recycle = recycle.next;
+ propertyRef.next = null;
+ }
+
+ ViewPropertyRef copy = null;
+ if (viewRefEnd.childCount < 0) {
+ copy = findInLastFrame(viewRefEnd.view.hashCode());
+ viewRefEnd.childCount = (copy != null) ? copy.childCount : 0;
+ }
+ viewRefEnd.transferTo(propertyRef);
+
+ if (resultStart == null) {
+ resultStart = propertyRef;
+ resultEnd = resultStart;
+ } else {
+ resultEnd.next = propertyRef;
+ resultEnd = resultEnd.next;
+ }
+
+ if (copy != null) {
+ int pending = copy.childCount;
+ while (pending > 0) {
+ copy = copy.next;
+ pending = pending - 1 + copy.childCount;
+
+ propertyRef = recycle;
+ if (propertyRef == null) {
+ propertyRef = new ViewPropertyRef();
+ } else {
+ recycle = recycle.next;
+ propertyRef.next = null;
+ }
+
+ copy.transferTo(propertyRef);
+
+ resultEnd.next = propertyRef;
+ resultEnd = resultEnd.next;
+ }
+ }
+
+ if (viewRefEnd.next == null) {
+ // The compiler will complain about using a non-final variable from
+ // an outer class in a lambda if we pass in viewRefEnd directly.
+ final ViewRef finalViewRefEnd = viewRefEnd;
+ MAIN_EXECUTOR.execute(() -> addToPool(viewRefStart, finalViewRefEnd));
+ break;
+ }
+ viewRefEnd = viewRefEnd.next;
+ }
+ mNodesBg[mFrameIndexBg] = resultStart;
+ }
+
+ private @Nullable ViewPropertyRef findInLastFrame(int hashCode) {
+ int lastFrameIndex = (mFrameIndexBg == 0) ? mMemorySize - 1 : mFrameIndexBg - 1;
+ ViewPropertyRef viewPropertyRef = mNodesBg[lastFrameIndex];
+ while (viewPropertyRef != null && viewPropertyRef.hashCode != hashCode) {
+ viewPropertyRef = viewPropertyRef.next;
+ }
+ return viewPropertyRef;
+ }
+
+ void attachToRoot() {
+ mIsActive = true;
+ if (mRoot.isAttachedToWindow()) {
+ safelyEnableOnDrawListener();
+ } else {
+ mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View v) {
+ if (mIsActive) {
+ safelyEnableOnDrawListener();
+ }
+ mRoot.removeOnAttachStateChangeListener(this);
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View v) {
+ }
+ });
+ }
+ }
+
+ void detachFromRoot() {
+ mIsActive = false;
+ if (mRoot != null) {
+ mRoot.getViewTreeObserver().removeOnDrawListener(this);
+ }
+ }
+
+ private void safelyEnableOnDrawListener() {
+ mRoot.getViewTreeObserver().removeOnDrawListener(this);
+ mRoot.getViewTreeObserver().addOnDrawListener(this);
+ }
+
+ @WorkerThread
+ private WindowData dumpToProto(ViewIdProvider idProvider, ArrayList<Class> classList) {
+ WindowData.Builder builder = WindowData.newBuilder().setTitle(name);
+ int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
+ for (int i = size - 1; i >= 0; i--) {
+ int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
+ ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
+ mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
+ FrameData.Builder frameDataBuilder = FrameData.newBuilder()
+ .setNode(nodeBuilder)
+ .setTimestamp(mFrameTimesNanosBg[index]);
+ builder.addFrameData(frameDataBuilder);
+ }
+ return builder.build();
+ }
+
+ private ViewRef captureViewTree(View view, ViewRef start) {
+ ViewRef ref;
+ if (mPool != null) {
+ ref = mPool;
+ mPool = mPool.next;
+ ref.next = null;
+ } else {
+ ref = new ViewRef();
+ }
+ ref.view = view;
+ start.next = ref;
+ if (view instanceof ViewGroup) {
+ ViewGroup parent = (ViewGroup) view;
+ // If a view has not changed since the last frame, we will copy
+ // its children from the last processed frame's data.
+ if ((view.mPrivateFlags & (PFLAG_INVALIDATED | PFLAG_DIRTY_MASK)) == 0
+ && !mIsFirstFrame) {
+ // A negative child count is the signal to copy this view from the last frame.
+ ref.childCount = -parent.getChildCount();
+ return ref;
+ }
+ ViewRef result = ref;
+ int childCount = ref.childCount = parent.getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ result = captureViewTree(parent.getChildAt(i), result);
+ }
+ return result;
+ } else {
+ ref.childCount = 0;
+ return ref;
+ }
+ }
+ }
+
+ private static class ViewPropertyRef {
+ // We store reference in memory to avoid generating and storing too many strings
+ public Class clazz;
+ public int hashCode;
+ public int childCount = 0;
+
+ public int id;
+ public int left, top, right, bottom;
+ public int scrollX, scrollY;
+
+ public float translateX, translateY;
+ public float scaleX, scaleY;
+ public float alpha;
+ public float elevation;
+
+ public int visibility;
+ public boolean willNotDraw;
+ public boolean clipChildren;
+
+ public ViewPropertyRef next;
+
+ public void transferTo(ViewPropertyRef out) {
+ out.clazz = this.clazz;
+ out.hashCode = this.hashCode;
+ out.childCount = this.childCount;
+ out.id = this.id;
+ out.left = this.left;
+ out.top = this.top;
+ out.right = this.right;
+ out.bottom = this.bottom;
+ out.scrollX = this.scrollX;
+ out.scrollY = this.scrollY;
+ out.scaleX = this.scaleX;
+ out.scaleY = this.scaleY;
+ out.translateX = this.translateX;
+ out.translateY = this.translateY;
+ out.alpha = this.alpha;
+ out.visibility = this.visibility;
+ out.willNotDraw = this.willNotDraw;
+ out.clipChildren = this.clipChildren;
+ out.elevation = this.elevation;
+ }
+
+ /**
+ * Converts the data to the proto representation and returns the next property ref
+ * at the end of the iteration.
+ */
+ public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
+ ViewNode.Builder viewNode) {
+ int classnameIndex = classList.indexOf(clazz);
+ if (classnameIndex < 0) {
+ classnameIndex = classList.size();
+ classList.add(clazz);
+ }
+
+ viewNode.setClassnameIndex(classnameIndex)
+ .setHashcode(hashCode)
+ .setId(idProvider.getName(id))
+ .setLeft(left)
+ .setTop(top)
+ .setWidth(right - left)
+ .setHeight(bottom - top)
+ .setTranslationX(translateX)
+ .setTranslationY(translateY)
+ .setScrollX(scrollX)
+ .setScrollY(scrollY)
+ .setScaleX(scaleX)
+ .setScaleY(scaleY)
+ .setAlpha(alpha)
+ .setVisibility(visibility)
+ .setWillNotDraw(willNotDraw)
+ .setElevation(elevation)
+ .setClipChildren(clipChildren);
+
+ ViewPropertyRef result = next;
+ for (int i = 0; (i < childCount) && (result != null); i++) {
+ ViewNode.Builder childViewNode = ViewNode.newBuilder();
+ result = result.toProto(idProvider, classList, childViewNode);
+ viewNode.addChildren(childViewNode);
+ }
+ return result;
+ }
+ }
+
+
+ private static class ViewRef implements Runnable {
+ public View view;
+ public int childCount = 0;
+ public ViewRef next;
+
+ public Consumer<ViewRef> callback = null;
+ public long choreographerTimeNanos = 0;
+
+ public void transferTo(ViewPropertyRef out) {
+ out.childCount = this.childCount;
+
+ View view = this.view;
+ this.view = null;
+
+ out.clazz = view.getClass();
+ out.hashCode = view.hashCode();
+ out.id = view.getId();
+ out.left = view.getLeft();
+ out.top = view.getTop();
+ out.right = view.getRight();
+ out.bottom = view.getBottom();
+ out.scrollX = view.getScrollX();
+ out.scrollY = view.getScrollY();
+
+ out.translateX = view.getTranslationX();
+ out.translateY = view.getTranslationY();
+ out.scaleX = view.getScaleX();
+ out.scaleY = view.getScaleY();
+ out.alpha = view.getAlpha();
+ out.elevation = view.getElevation();
+
+ out.visibility = view.getVisibility();
+ out.willNotDraw = view.willNotDraw();
+ }
+
+ @Override
+ public void run() {
+ Consumer<ViewRef> oldCallback = callback;
+ callback = null;
+ if (oldCallback != null) {
+ oldCallback.accept(this);
+ }
+ }
+ }
+
+ private static final class ViewIdProvider {
+
+ private final SparseArray<String> mNames = new SparseArray<>();
+ private final Resources mRes;
+
+ ViewIdProvider(Resources res) {
+ mRes = res;
+ }
+
+ String getName(int id) {
+ String name = mNames.get(id);
+ if (name == null) {
+ if (id >= 0) {
+ try {
+ name = mRes.getResourceTypeName(id) + '/' + mRes.getResourceEntryName(id);
+ } catch (Resources.NotFoundException e) {
+ name = "id/" + "0x" + Integer.toHexString(id).toUpperCase();
+ }
+ } else {
+ name = "NO_ID";
+ }
+ mNames.put(id, name);
+ }
+ return name;
+ }
+ }
+}
diff --git a/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto
new file mode 100644
index 0000000..d4df2ae
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/proto/view_capture.proto
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 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.
+ */
+
+syntax = "proto2";
+
+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;
+}
+
+message FrameData {
+ optional int64 timestamp = 1; // choreographer timestamp in nanoseconds
+ optional ViewNode node = 2;
+}
+
+message ViewNode {
+ optional int32 classname_index = 1;
+ optional int32 hashcode = 2;
+
+ repeated ViewNode children = 3;
+
+ optional string id = 4;
+ optional int32 left = 5;
+ optional int32 top = 6;
+ optional int32 width = 7;
+ optional int32 height = 8;
+ optional int32 scrollX = 9;
+ optional int32 scrollY = 10;
+
+ optional float translationX = 11;
+ optional float translationY = 12;
+ optional float scaleX = 13 [default = 1];
+ optional float scaleY = 14 [default = 1];
+ optional float alpha = 15 [default = 1];
+
+ optional bool willNotDraw = 16;
+ optional bool clipChildren = 17;
+ optional int32 visibility = 18;
+
+ optional float elevation = 19;
+}
diff --git a/viewcapturelib/tests/AndroidManifest.xml b/viewcapturelib/tests/AndroidManifest.xml
new file mode 100644
index 0000000..8d31c0e
--- /dev/null
+++ b/viewcapturelib/tests/AndroidManifest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2022 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.viewcapture.test">
+ <application
+ android:debuggable="true"
+ android:theme="@android:style/Theme.NoTitleBar">
+
+ <activity
+ android:name="com.android.app.viewcapture.TestActivity"
+ android:exported="false" />
+
+ <uses-library android:name="android.test.runner" />
+
+ </application>
+
+ <instrumentation
+ android:name="android.testing.TestableInstrumentation"
+ 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
new file mode 100644
index 0000000..2157ee4
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.viewcapture
+
+import android.Manifest
+import android.content.Context
+import android.content.Intent
+import android.media.permission.SafeCloseable
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+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
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class SettingsAwareViewCaptureTest {
+ private val context: Context = InstrumentationRegistry.getInstrumentation().context
+ 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() {
+ Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 0)
+
+ activityScenarioRule.scenario.onActivity { activity ->
+ val viewCapture: ViewCapture =
+ SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+ val rootView: View = activity.findViewById(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)
+ closeable.close()
+ }
+ }
+ }
+
+ @Test
+ fun capture_view_hierarchies_if_setting_is_enabled() {
+ Settings.Global.putInt(context.contentResolver, VIEW_CAPTURE_ENABLED, 1)
+
+ activityScenarioRule.scenario.onActivity { activity ->
+ val viewCapture: ViewCapture =
+ SettingsAwareViewCapture(context, Choreographer.getInstance(), MAIN_EXECUTOR)
+ val rootView: View = activity.findViewById(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)
+
+ closeable.close()
+ }
+ }
+ }
+
+ @Test
+ fun getInstance_calledTwiceInARow_returnsSameObject() {
+ assertEquals(
+ SettingsAwareViewCapture.getInstance(context).hashCode(),
+ SettingsAwareViewCapture.getInstance(context).hashCode()
+ )
+ }
+}
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
new file mode 100644
index 0000000..749327e
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.viewcapture
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.LinearLayout
+import android.widget.TextView
+
+/**
+ * Activity with the content set to a [LinearLayout] with [TextView] children.
+ */
+class TestActivity : Activity() {
+
+ companion object {
+ const val TEXT_VIEW_COUNT = 1000
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(createContentView())
+ }
+
+ private fun createContentView(): LinearLayout {
+ val root = LinearLayout(this)
+ for (i in 0 until TEXT_VIEW_COUNT) {
+ root.addView(TextView(this))
+ }
+ return root
+ }
+} \ No newline at end of file
diff --git a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
new file mode 100644
index 0000000..b341fe9
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2022 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.viewcapture
+
+import android.content.Intent
+import android.media.permission.SafeCloseable
+import android.testing.AndroidTestingRunner
+import android.view.Choreographer
+import android.view.View
+import android.widget.LinearLayout
+import android.widget.TextView
+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.MotionWindowData
+import junit.framework.Assert.assertEquals
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class ViewCaptureTest {
+
+ private val memorySize = 100
+ private val initPoolSize = 15
+ private val viewCapture by lazy {
+ object :
+ ViewCapture(memorySize, initPoolSize, Choreographer.getInstance(), MAIN_EXECUTOR) {}
+ }
+
+ private val activityIntent =
+ Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
+
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
+
+ @Test
+ fun testWindowListenerDumpsOneFrameAfterInvalidate() {
+ activityScenarioRule.scenario.onActivity { activity ->
+ Choreographer.getInstance().postFrameCallback {
+ val closeable = startViewCaptureAndInvalidateNTimes(1, activity)
+ val rootView = activity.findViewById<View>(android.R.id.content)
+ val data = viewCapture.getDumpTask(rootView).get().get()
+
+ assertEquals(1, data.frameDataList.size)
+ verifyTestActivityViewHierarchy(data)
+ closeable.close()
+ }
+ }
+ }
+
+ @Test
+ 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 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, 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 closeable: SafeCloseable = viewCapture.startCapture(rootView, "rootViewId")
+ dispatchOnDraw(rootView, times = n)
+ return closeable
+ }
+
+ private fun dispatchOnDraw(view: View, times: Int) {
+ if (times > 0) {
+ view.viewTreeObserver.dispatchOnDraw()
+ dispatchOnDraw(view, times - 1)
+ }
+ }
+
+ private fun verifyTestActivityViewHierarchy(exportedData: MotionWindowData) {
+ for (frame in exportedData.frameDataList) {
+ val testActivityRoot =
+ frame.node // FrameLayout (android.R.id.content)
+ .childrenList
+ .first() // LinearLayout (set by setContentView())
+ assertEquals(TEXT_VIEW_COUNT, testActivityRoot.childrenList.size)
+ assertEquals(
+ LinearLayout::class.qualifiedName,
+ exportedData.getClassname(testActivityRoot.classnameIndex)
+ )
+ assertEquals(
+ TextView::class.qualifiedName,
+ exportedData.getClassname(testActivityRoot.childrenList.first().classnameIndex)
+ )
+ }
+ }
+}