summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 15:00:14 +0530
committerGeorge Zacharia <george.zcharia@gmail.com>2023-07-02 15:00:14 +0530
commite1fe4bad8afa333499590808f3994c8bea0c833b (patch)
tree84db2665b4d6f4e0ae52cecf10eef27facf72cb5
parent2d42f0cd9b97e656643049457e668864063a7051 (diff)
parent313ca2b3cbaef97cb4665b24412eccf3d4cdcae3 (diff)
Merge tag 'android-13.0.0_r52' of https://android.googlesource.com/platform/frameworks/libs/systemui into HEADt13.0
Android 13.0.0 Release 52 (TQ3A.230605.012) Change-Id: I1a59918e5a17f17968121c15476f31567c3d257a
-rw-r--r--iconloaderlib/res/drawable/ic_clone_app_badge.xml43
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java33
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java4
-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/ThemedIconDrawable.java5
-rw-r--r--iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java31
-rw-r--r--motiontoollib/Android.bp4
-rw-r--r--motiontoollib/build.gradle25
-rw-r--r--motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt110
-rw-r--r--motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt46
-rw-r--r--motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt109
-rw-r--r--motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt41
-rw-r--r--searchuilib/Android.bp6
-rw-r--r--searchuilib/src/com/android/app/search/LayoutType.java23
-rw-r--r--searchuilib/src/com/android/app/search/QueryExtras.java42
-rw-r--r--searchuilib/src/com/android/app/search/ResultType.java38
-rw-r--r--searchuilib/src/com/android/app/search/SearchActionExtras.java40
-rw-r--r--searchuilib/src/com/android/app/search/SearchTargetConverter.java69
-rw-r--r--searchuilib/src/com/android/app/search/SearchTargetEventHelper.java106
-rw-r--r--searchuilib/src/com/android/app/search/SearchTargetExtras.java228
-rw-r--r--searchuilib/src/com/android/app/search/SearchTargetGenerator.java44
-rw-r--r--viewcapturelib/Android.bp4
-rw-r--r--viewcapturelib/AndroidManifest.xml3
-rw-r--r--viewcapturelib/build.gradle26
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java2
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt83
-rw-r--r--viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java171
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt92
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/TestActivity.kt45
-rw-r--r--viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt123
32 files changed, 1230 insertions, 390 deletions
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/src/com/android/launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
index cbd46b3..2f85193 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BaseIconFactory.java
@@ -5,6 +5,7 @@ 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;
@@ -214,7 +215,8 @@ public class BaseIconFactory implements AutoCloseable {
boolean shrinkNonAdaptiveIcons = options == null || options.mShrinkNonAdaptiveIcons;
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
- Bitmap bitmap = createIconBitmap(icon, scale[0], MODE_WITH_SHADOW);
+ Bitmap bitmap = createIconBitmap(icon, scale[0],
+ options == null ? MODE_WITH_SHADOW : options.mGenerationMode);
int color = (options != null && options.mExtractedColor != null)
? options.mExtractedColor : mColorExtractor.findDominantColorByHue(bitmap);
@@ -267,7 +269,10 @@ 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;
@@ -469,6 +474,11 @@ public class BaseIconFactory implements AutoCloseable {
boolean mIsInstantApp;
+ boolean mIsCloneProfile;
+
+ @BitmapGenerationMode
+ int mGenerationMode = MODE_WITH_SHADOW;
+
@Nullable UserHandle mUserHandle;
@ColorInt
@@ -509,6 +519,25 @@ public class BaseIconFactory implements AutoCloseable {
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;
+ }
}
/**
diff --git a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
index c3ca42e..37ad04d 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/BitmapInfo.java
@@ -30,9 +30,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 {}
@@ -155,6 +157,8 @@ public class BitmapInfo {
drawable.setBadge(context.getDrawable(R.drawable.ic_instant_app_badge));
} else if ((flags & FLAG_WORK) != 0) {
drawable.setBadge(context.getDrawable(R.drawable.ic_work_app_badge));
+ } else if ((flags & FLAG_CLONE) != 0) {
+ drawable.setBadge(context.getDrawable(R.drawable.ic_clone_app_badge));
}
}
}
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 03da09a..b3afa94 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/IconProvider.java
@@ -66,7 +66,7 @@ import java.util.function.Supplier;
public class IconProvider implements ResourceBasedOverride {
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/ThemedIconDrawable.java b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
index 494d657..8442eed 100644
--- a/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
+++ b/iconloaderlib/src/com/android/launcher3/icons/ThemedIconDrawable.java
@@ -92,8 +92,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 {
diff --git a/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java b/iconloaderlib/src/com/android/launcher3/icons/cache/BaseIconCache.java
index 7d396c0..a785e05 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;
@@ -79,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 = ".";
@@ -122,6 +125,8 @@ public abstract class BaseIconCache {
@NonNull
private final SparseArray<FlagOp> mUserFlagOpMap = new SparseArray<>();
+ private final SparseArray<String> mUserFormatString = new SparseArray<>();
+
@Nullable
private final String mDbFileName;
@@ -268,6 +273,7 @@ public abstract class BaseIconCache {
private void updateSystemState() {
mLocaleList = mContext.getResources().getConfiguration().getLocales();
mSystemState = mLocaleList.toLanguageTags() + "," + Build.VERSION.INCREMENTAL;
+ mUserFormatString.clear();
}
@NonNull
@@ -275,6 +281,22 @@ public abstract class BaseIconCache {
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
@@ -313,7 +335,7 @@ public abstract class BaseIconCache {
}
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(),
@@ -468,7 +490,7 @@ public abstract class BaseIconCache {
@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);
}
@@ -549,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);
@@ -610,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) {
diff --git a/motiontoollib/Android.bp b/motiontoollib/Android.bp
index 911be1d..6762d83 100644
--- a/motiontoollib/Android.bp
+++ b/motiontoollib/Android.bp
@@ -20,7 +20,7 @@ java_library {
name: "motion_tool_proto",
srcs: ["src/com/android/app/motiontool/proto/*.proto"],
proto: {
- type: "nano",
+ type: "lite",
local_include_dirs:[
"src/com/android/app/motiontool/proto"
],
@@ -29,7 +29,7 @@ java_library {
],
},
static_libs: [
- "libprotobuf-java-nano",
+ "libprotobuf-java-lite",
"view_capture_proto",
],
java_version: "1.8",
diff --git a/motiontoollib/build.gradle b/motiontoollib/build.gradle
index 2a25184..a1f39b9 100644
--- a/motiontoollib/build.gradle
+++ b/motiontoollib/build.gradle
@@ -38,26 +38,31 @@ android {
dependencies {
implementation "androidx.core:core:1.9.0"
- implementation PROTOBUF_DEPENDENCY
+ implementation "com.google.protobuf:protobuf-lite:${protobuf_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}"
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- remove java
- javanano {
- option "enum_style=c"
- }
- }
+ }
+ 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
index 3f608d5..63a6fee 100644
--- a/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
+++ b/motiontoollib/src/com/android/app/motiontool/DdmHandleMotionTool.kt
@@ -17,9 +17,7 @@
package com.android.app.motiontool
import android.ddm.DdmHandle
-import com.android.app.motiontool.nano.*
-import com.google.protobuf.nano.InvalidProtocolBufferNanoException
-import com.google.protobuf.nano.MessageNano
+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
@@ -67,19 +65,18 @@ class DdmHandleMotionTool private constructor(
val protoRequest =
try {
MotionToolsRequest.parseFrom(requestDataBuffer.array())
- } catch (e: InvalidProtocolBufferNanoException) {
- val parseErrorResponse =
- ErrorResponse().apply {
- code = ErrorResponse.INVALID_REQUEST
- message = "Invalid request format (Protobuf parse exception)"
- }
- val wrappedResponse = MotionToolsResponse().apply { error = parseErrorResponse }
- val responseData = MessageNano.toByteArray(wrappedResponse)
+ } 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) {
+ when (protoRequest.typeCase.number) {
MotionToolsRequest.HANDSHAKE_FIELD_NUMBER ->
handleHandshakeRequest(protoRequest.handshake)
MotionToolsRequest.BEGIN_TRACE_FIELD_NUMBER ->
@@ -89,87 +86,76 @@ class DdmHandleMotionTool private constructor(
MotionToolsRequest.END_TRACE_FIELD_NUMBER ->
handleEndTraceRequest(protoRequest.endTrace)
else ->
- MotionToolsResponse().apply {
- error =
- ErrorResponse().apply {
- code = ErrorResponse.INVALID_REQUEST
- message = "Unknown request type"
- }
- }
+ MotionToolsResponse.newBuilder().setError(ErrorResponse.newBuilder()
+ .setCode(ErrorResponse.Code.INVALID_REQUEST)
+ .setMessage("Unknown request type")).build()
}
- val responseData = MessageNano.toByteArray(response)
+ val responseData = response.toByteArray()
return Chunk(CHUNK_MOTO, responseData, 0, responseData.size)
}
- private fun handleBeginTraceRequest(beginTraceRequest: BeginTraceRequest) =
- MotionToolsResponse().apply {
+ private fun handleBeginTraceRequest(beginTraceRequest: BeginTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
tryCatchingMotionToolManagerExceptions {
- beginTrace =
- BeginTraceResponse().apply {
- traceId = motionToolManager.beginTrace(beginTraceRequest.window.rootWindow)
- }
+ setBeginTrace(BeginTraceResponse.newBuilder().setTraceId(
+ motionToolManager.beginTrace(beginTraceRequest.window.rootWindow)))
}
- }
+ }.build()
- private fun handlePollTraceRequest(pollTraceRequest: PollTraceRequest) =
- MotionToolsResponse().apply {
+ private fun handlePollTraceRequest(pollTraceRequest: PollTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
tryCatchingMotionToolManagerExceptions {
- pollTrace =
- PollTraceResponse().apply {
- exportedData = motionToolManager.pollTrace(pollTraceRequest.traceId)
- }
+ setPollTrace(PollTraceResponse.newBuilder()
+ .setExportedData(motionToolManager.pollTrace(pollTraceRequest.traceId)))
}
- }
+ }.build()
- private fun handleEndTraceRequest(endTraceRequest: EndTraceRequest) =
- MotionToolsResponse().apply {
+ private fun handleEndTraceRequest(endTraceRequest: EndTraceRequest): MotionToolsResponse =
+ MotionToolsResponse.newBuilder().apply {
tryCatchingMotionToolManagerExceptions {
- endTrace =
- EndTraceResponse().apply {
- exportedData = motionToolManager.endTrace(endTraceRequest.traceId)
- }
+ setEndTrace(EndTraceResponse.newBuilder()
+ .setExportedData(motionToolManager.endTrace(endTraceRequest.traceId)))
}
- }
-
- private fun handleHandshakeRequest(handshakeRequest: HandshakeRequest) =
- MotionToolsResponse().apply {
- handshake =
- HandshakeResponse().apply {
- serverVersion = SERVER_VERSION
- status =
- if (motionToolManager.hasWindow(handshakeRequest.window)) {
- HandshakeResponse.OK
- } else {
- HandshakeResponse.WINDOW_NOT_FOUND
- }
- }
- }
+ }.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.tryCatchingMotionToolManagerExceptions(block: () -> Unit) {
+ private fun MotionToolsResponse.Builder.tryCatchingMotionToolManagerExceptions(block: () -> Unit) {
try {
block()
} catch (e: UnknownTraceIdException) {
- error = createUnknownTraceIdResponse(e.traceId)
+ setError(createUnknownTraceIdResponse(e.traceId))
} catch (e: WindowNotFoundException) {
- error = createWindowNotFoundResponse(e.windowId)
+ setError(createWindowNotFoundResponse(e.windowId))
}
}
private fun createUnknownTraceIdResponse(traceId: Int) =
- ErrorResponse().apply {
- this.code = ErrorResponse.UNKNOWN_TRACE_ID
+ 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().apply {
- this.code = ErrorResponse.WINDOW_NOT_FOUND
+ ErrorResponse.newBuilder().apply {
+ this.code = ErrorResponse.Code.WINDOW_NOT_FOUND
this.message = "No window found with windowId $windowId"
}
diff --git a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
index f97bb5d..66b00f7 100644
--- a/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
+++ b/motiontoollib/src/com/android/app/motiontool/MotionToolManager.kt
@@ -16,13 +16,14 @@
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.motiontool.nano.WindowIdentifier
import com.android.app.viewcapture.ViewCapture
-import com.android.app.viewcapture.data.nano.ExportedData
+import com.android.app.viewcapture.data.ExportedData
/**
* Singleton to manage motion tracing sessions.
@@ -41,10 +42,8 @@ import com.android.app.viewcapture.data.nano.ExportedData
*
* @see [DdmHandleMotionTool]
*/
-class MotionToolManager private constructor(
- private val viewCapture: ViewCapture,
- private val windowManagerGlobal: WindowManagerGlobal
-) {
+class MotionToolManager private constructor(private val windowManagerGlobal: WindowManagerGlobal) {
+ private val viewCapture: ViewCapture = SimpleViewCapture()
companion object {
private const val TAG = "MotionToolManager"
@@ -52,13 +51,8 @@ class MotionToolManager private constructor(
private var INSTANCE: MotionToolManager? = null
@Synchronized
- fun getInstance(
- viewCapture: ViewCapture,
- windowManagerGlobal: WindowManagerGlobal
- ): MotionToolManager {
- return INSTANCE ?: MotionToolManager(viewCapture, windowManagerGlobal).also {
- INSTANCE = it
- }
+ fun getInstance(windowManagerGlobal: WindowManagerGlobal): MotionToolManager {
+ return INSTANCE ?: MotionToolManager(windowManagerGlobal).also { INSTANCE = it }
}
}
@@ -125,20 +119,28 @@ class MotionToolManager private constructor(
val rootView =
getRootView(traceMetadata.windowId)
?: throw WindowNotFoundException(traceMetadata.windowId)
- return viewCapture
+
+ val exportedData = viewCapture
.getDumpTask(rootView)
?.orElse(null)
- ?.get()
- ?.apply {
- frameData = frameData?.filter { it.timestamp > traceMetadata.lastPolledTime }
- ?.toTypedArray()
- }
- ?: ExportedData()
+ ?.get() ?: return ExportedData.newBuilder().build()
+
+ val filteredFrameData = exportedData.frameDataList
+ ?.filter { it.timestamp > traceMetadata.lastPolledTime }
+
+ return exportedData.toBuilder()
+ .clearFrameData()
+ .addAllFrameData(filteredFrameData)
+ .build()
}
private fun getRootView(windowId: String): View? {
return windowManagerGlobal.getRootView(windowId)
}
+
+ class SimpleViewCapture : ViewCapture(DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE,
+ MAIN_EXECUTOR.submit { Choreographer.getInstance() }.get(),
+ createAndStartNewLooperExecutor("MTViewCapture", Process.THREAD_PRIORITY_FOREGROUND))
}
private data class TraceMetadata(
@@ -147,7 +149,7 @@ private data class TraceMetadata(
var stopTrace: () -> Unit
) {
fun updateLastPolledTime(exportedData: ExportedData?) {
- exportedData?.frameData?.maxOfOrNull { it.timestamp }?.let { maxFrameTimestamp ->
+ exportedData?.frameDataList?.maxOfOrNull { it.timestamp }?.let { maxFrameTimestamp ->
lastPolledTime = maxFrameTimestamp
}
}
@@ -155,4 +157,4 @@ private data class TraceMetadata(
class UnknownTraceIdException(val traceId: Int) : Exception()
-class WindowNotFoundException(val windowId: String) : Exception()
+class WindowNotFoundException(val windowId: String) : Exception() \ No newline at end of file
diff --git a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
index 5627106..7d78237 100644
--- a/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
+++ b/motiontoollib/tests/com/android/app/motiontool/DdmHandleMotionToolTest.kt
@@ -18,24 +18,14 @@ 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.nano.BeginTraceRequest
-import com.android.app.motiontool.nano.EndTraceRequest
-import com.android.app.motiontool.nano.ErrorResponse
-import com.android.app.motiontool.nano.HandshakeRequest
-import com.android.app.motiontool.nano.HandshakeResponse
-import com.android.app.motiontool.nano.MotionToolsRequest
-import com.android.app.motiontool.nano.MotionToolsResponse
-import com.android.app.motiontool.nano.PollTraceRequest
-import com.android.app.motiontool.nano.WindowIdentifier
import com.android.app.motiontool.util.TestActivity
-import com.android.app.viewcapture.ViewCapture
-import com.google.protobuf.nano.MessageNano
import junit.framework.Assert
import junit.framework.Assert.assertEquals
import org.apache.harmony.dalvik.ddmc.Chunk
@@ -46,17 +36,12 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DdmHandleMotionToolTest {
- private val viewCaptureMemorySize = 100
- private val viewCaptureInitPoolSize = 15
- private val viewCapture =
- ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
private val windowManagerGlobal = WindowManagerGlobal.getInstance()
- private val motionToolManager = MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
private val ddmHandleMotionTool = DdmHandleMotionTool.getInstance(motionToolManager)
private val CLIENT_VERSION = 1
@@ -74,51 +59,50 @@ class DdmHandleMotionToolTest {
@After
fun cleanup() {
ddmHandleMotionTool.unregister()
- motionToolManager.reset()
}
@Test
fun testHandshakeErrorWithInvalidWindowId() {
val handshakeResponse = performHandshakeRequest("InvalidWindowId")
- assertEquals(HandshakeResponse.WINDOW_NOT_FOUND, handshakeResponse.handshake.status)
+ assertEquals(HandshakeResponse.Status.WINDOW_NOT_FOUND, handshakeResponse.handshake.status)
}
@Test
fun testHandshakeOkWithValidWindowId() {
val handshakeResponse = performHandshakeRequest(getActivityViewRootId())
- assertEquals(HandshakeResponse.OK, handshakeResponse.handshake.status)
+ assertEquals(HandshakeResponse.Status.OK, handshakeResponse.handshake.status)
}
@Test
fun testBeginFailsWithInvalidWindowId() {
val errorResponse = performBeginTraceRequest("InvalidWindowId")
- assertEquals(ErrorResponse.WINDOW_NOT_FOUND, errorResponse.error.code)
+ assertEquals(ErrorResponse.Code.WINDOW_NOT_FOUND, errorResponse.error.code)
}
@Test
fun testEndTraceFailsWithoutPrecedingBeginTrace() {
val errorResponse = performEndTraceRequest(0)
- assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, errorResponse.error.code)
}
@Test
fun testPollTraceFailsWithoutPrecedingBeginTrace() {
val errorResponse = performPollTraceRequest(0)
- assertEquals(ErrorResponse.UNKNOWN_TRACE_ID, errorResponse.error.code)
+ 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.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ 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.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
+ assertEquals(ErrorResponse.Code.UNKNOWN_TRACE_ID, endTraceResponse.error.code)
}
@Test
@@ -127,7 +111,7 @@ class DdmHandleMotionToolTest {
val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
val response = MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array()).error
- assertEquals(ErrorResponse.INVALID_REQUEST, response.code)
+ assertEquals(ErrorResponse.Code.INVALID_REQUEST, response.code)
}
@Test
@@ -135,75 +119,66 @@ class DdmHandleMotionToolTest {
activityScenarioRule.scenario.onActivity {
val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
val endTraceResponse = performEndTraceRequest(beginTraceResponse.beginTrace.traceId)
- Assert.assertTrue(endTraceResponse.endTrace.exportedData.frameData.isEmpty())
+ Assert.assertTrue(endTraceResponse.endTrace.exportedData.frameDataList.isEmpty())
}
}
@Test
fun testOneOnDrawCallReturnsOneFrameResponse() {
- var traceId = 0
- activityScenarioRule.scenario.onActivity {
+ activityScenarioRule.scenario.onActivity { activity ->
val beginTraceResponse = performBeginTraceRequest(getActivityViewRootId())
- traceId = beginTraceResponse.beginTrace.traceId
- val rootView = it.findViewById<View>(android.R.id.content)
- rootView.invalidate()
- }
+ val traceId = beginTraceResponse.beginTrace.traceId
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val pollTraceResponse = performPollTraceRequest(traceId)
- assertEquals(1, pollTraceResponse.pollTrace.exportedData.frameData.size)
+ Choreographer.getInstance().postFrameCallback {
+ activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
- // Verify that frameData is only included once and is not returned again
- val endTraceResponse = performEndTraceRequest(traceId)
- assertEquals(0, endTraceResponse.endTrace.exportedData.frameData.size)
- }
+ val pollTraceResponse = performPollTraceRequest(traceId)
+ assertEquals(1, pollTraceResponse.pollTrace.exportedData.frameDataList.size)
+ // Verify that frameData is only included once and is not returned again
+ val endTraceResponse = performEndTraceRequest(traceId)
+ assertEquals(0, endTraceResponse.endTrace.exportedData.frameDataList.size)
+ }
+ }
}
private fun performPollTraceRequest(requestTraceId: Int): MotionToolsResponse {
- val pollTraceRequest = MotionToolsRequest().apply {
- pollTrace = PollTraceRequest().apply {
- traceId = requestTraceId
- }
- }
+ val pollTraceRequest = MotionToolsRequest.newBuilder()
+ .setPollTrace(PollTraceRequest.newBuilder()
+ .setTraceId(requestTraceId))
+ .build()
return performRequest(pollTraceRequest)
}
private fun performEndTraceRequest(requestTraceId: Int): MotionToolsResponse {
- val endTraceRequest = MotionToolsRequest().apply {
- endTrace = EndTraceRequest().apply {
- traceId = requestTraceId
- }
- }
+ val endTraceRequest = MotionToolsRequest.newBuilder()
+ .setEndTrace(EndTraceRequest.newBuilder()
+ .setTraceId(requestTraceId))
+ .build()
return performRequest(endTraceRequest)
}
private fun performBeginTraceRequest(windowId: String): MotionToolsResponse {
- val beginTraceRequest = MotionToolsRequest().apply {
- beginTrace = BeginTraceRequest().apply {
- window = WindowIdentifier().apply {
- rootWindow = windowId
- }
- }
- }
+ 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().apply {
- handshake = HandshakeRequest().apply {
- window = WindowIdentifier().apply {
- rootWindow = windowId
- }
- clientVersion = CLIENT_VERSION
- }
- }
+ 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 = MessageNano.toByteArray(motionToolsRequest)
+ val requestBytes = motionToolsRequest.toByteArray()
val requestChunk = Chunk(CHUNK_MOTO, requestBytes, 0, requestBytes.size)
val responseChunk = ddmHandleMotionTool.handleChunk(requestChunk)
return MotionToolsResponse.parseFrom(wrapChunk(responseChunk).array())
diff --git a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
index 02751fb..560f798 100644
--- a/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
+++ b/motiontoollib/tests/com/android/app/motiontool/MotionToolManagerTest.kt
@@ -18,31 +18,25 @@ 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 com.android.app.viewcapture.ViewCapture
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertTrue
-import org.junit.After
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 viewCaptureMemorySize = 100
- private val viewCaptureInitPoolSize = 15
- private val viewCapture =
- ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
- private val motionToolManager = MotionToolManager.getInstance(viewCapture, windowManagerGlobal)
+ private val motionToolManager = MotionToolManager.getInstance(windowManagerGlobal)
private val activityIntent =
Intent(InstrumentationRegistry.getInstrumentation().context, TestActivity::class.java)
@@ -50,11 +44,6 @@ class MotionToolManagerTest {
@get:Rule
val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
- @After
- fun cleanup() {
- motionToolManager.reset()
- }
-
@Test(expected = UnknownTraceIdException::class)
fun testEndTraceThrowsWithoutPrecedingBeginTrace() {
motionToolManager.endTrace(0)
@@ -87,29 +76,25 @@ class MotionToolManagerTest {
activityScenarioRule.scenario.onActivity {
val traceId = motionToolManager.beginTrace(getActivityViewRootId())
val result = motionToolManager.endTrace(traceId)
- assertTrue(result.frameData.isEmpty())
+ assertTrue(result.frameDataList.isEmpty())
}
}
@Test
fun testOneOnDrawCallReturnsOneFrameResponse() {
- var traceId = 0
- activityScenarioRule.scenario.onActivity {
- traceId = motionToolManager.beginTrace(getActivityViewRootId())
- val rootView = it.findViewById<View>(android.R.id.content)
- rootView.invalidate()
- }
+ activityScenarioRule.scenario.onActivity { activity ->
+ val traceId = motionToolManager.beginTrace(getActivityViewRootId())
+ Choreographer.getInstance().postFrameCallback {
+ activity.findViewById<View>(android.R.id.content).viewTreeObserver.dispatchOnDraw()
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val polledExportedData = motionToolManager.pollTrace(traceId)
- assertEquals(1, polledExportedData.frameData.size)
+ val polledExportedData = motionToolManager.pollTrace(traceId)
+ assertEquals(1, polledExportedData.frameDataList.size)
- // Verify that frameData is only included once and is not returned again
- val endExportedData = motionToolManager.endTrace(traceId)
- assertEquals(0, endExportedData.frameData.size)
+ // Verify that frameData is only included once and is not returned again
+ val endExportedData = motionToolManager.endTrace(traceId)
+ assertEquals(0, endExportedData.frameDataList.size)
+ }
}
-
}
private fun getActivityViewRootId(): String {
diff --git a/searchuilib/Android.bp b/searchuilib/Android.bp
index 2b25616..f7b0b83 100644
--- a/searchuilib/Android.bp
+++ b/searchuilib/Android.bp
@@ -19,9 +19,11 @@ package {
android_library {
name: "search_ui",
- sdk_version: "current",
- min_sdk_version: "26",
+ sdk_version: "system_current",
+ static_libs: [
+ "androidx.annotation_annotation",
+ ],
srcs: [
"src/**/*.java",
],
diff --git a/searchuilib/src/com/android/app/search/LayoutType.java b/searchuilib/src/com/android/app/search/LayoutType.java
index 1422bb9..53c663d 100644
--- a/searchuilib/src/com/android/app/search/LayoutType.java
+++ b/searchuilib/src/com/android/app/search/LayoutType.java
@@ -47,7 +47,14 @@ public class LayoutType {
public static final String SMALL_ICON_HORIZONTAL_TEXT = "short_icon_row";
public static final String SMALL_ICON_HORIZONTAL_TEXT_THUMBNAIL = "short_icon_row_thumbnail";
- // This layout contains a series of thumbnails (currently up to 3 per row)
+ // This layout contains a series of icon results (currently up to 4 per row).
+ // The container does not support stretching for its children, and can only contain
+ // {@link #ICON_SINGLE_VERTICAL_TEXT} layout types.
+ public static final String ICON_CONTAINER = "icon_container";
+
+ // This layout contains a series of thumbnails (currently up to 3 per row).
+ // The container supports stretching for its children, and can only contain {@link #THUMBNAIL}
+ // layout types.
public static final String THUMBNAIL_CONTAINER = "thumbnail_container";
// This layout creates a container for people grouping
@@ -74,9 +81,6 @@ public class LayoutType {
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
@@ -92,4 +96,15 @@ public class LayoutType {
// Layout for a text header
// Available for SearchUiManager proxy service to use above version code 3
public static final String TEXT_HEADER_ROW = "text_header_row";
+
+ // Layout for a quick settings tile
+ public static final String QS_TILE = "qs_tile";
+
+ // Placeholder for web suggest.
+ public static final String PLACEHOLDER = "placeholder";
+
+ // Placeholder for rich answer cards.
+ // Only available on or above version code 3.
+ public static final String RICHANSWER_PLACEHOLDER = "richanswer_placeholder";
+
}
diff --git a/searchuilib/src/com/android/app/search/QueryExtras.java b/searchuilib/src/com/android/app/search/QueryExtras.java
new file mode 100644
index 0000000..a6dbba1
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/QueryExtras.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.search;
+
+import android.app.search.Query;
+
+/**
+ * Utility class used to define implicit contract between aiai and launcher regarding
+ * what constant string key should be used to pass sub session information inside
+ * the {@link Query} object.
+ *
+ * This decorated query object is passed to aiai using two method calls:
+ * <ul>
+ * <ol>android.app.search.SearchSession.query()</ol>
+ * <ol>android.app.search.SearchSession.notifyEvent()</ol>
+ * </ul>
+ */
+public class QueryExtras {
+
+ // Can be either 1 (ALLAPPS) or 2 (QSB)
+ public static final String EXTRAS_KEY_ENTRY = "entry";
+
+ // This value overrides the timeout that is defined inside {@link SearchContext#getTimeout}
+ public static final String EXTRAS_KEY_TIMEOUT_OVERRIDE = "timeout";
+
+ // Used to know which target is deleted.
+ public static final String EXTRAS_BUNDLE_DELETED_TARGET_ID = "deleted_target_id";
+}
diff --git a/searchuilib/src/com/android/app/search/ResultType.java b/searchuilib/src/com/android/app/search/ResultType.java
index 472434c..9dd9dad 100644
--- a/searchuilib/src/com/android/app/search/ResultType.java
+++ b/searchuilib/src/com/android/app/search/ResultType.java
@@ -51,4 +51,42 @@ public class ResultType {
public static final int EDUCARD = 1 << 19;
public static final int SYSTEM_POINTER = 1 << 20;
public static final int VIDEO = 1 << 21;
+
+ public static final int PUBLIC_DATA_TYPE = APPLICATION | SETTING | PLAY | WEB_SUGGEST;
+ public static final int PRIMITIVE_TYPE = APPLICATION | SLICE | SHORTCUT | WIDGETS | ACTION |
+ LEGACY_SHORTCUT;
+ public static final int CORPUS_TYPE =
+ PEOPLE | SETTING | IMAGE | PLAY | SUGGEST | ASSISTANT | CHROMETAB | NAVVYSITE | TIPS
+ | PEOPLE_TILE | MEMORY | WEB_SUGGEST | VIDEO;
+ public static final int RANK_TYPE = SYSTEM_POINTER;
+ public static final int UI_TYPE = EDUCARD | NO_FULFILLMENT;
+
+ public static boolean isSlice(int resultType) {
+ return (resultType & SLICE) != 0;
+ }
+
+ public static boolean isSystemPointer(int resultType) {
+ return (resultType & SYSTEM_POINTER) != 0;
+ }
+
+ /**
+ * Returns result type integer where only {@code #CORPUS_TYPE} bit will turned on.
+ */
+ public static int getCorpusType(int resultType) {
+ return (resultType & CORPUS_TYPE);
+ }
+
+ /**
+ * Returns result type integer where only {@code #PRIMITIVE_TYPE} bit will be turned on.
+ */
+ public static int getPrimitiveType(int resultType) {
+ return (resultType & PRIMITIVE_TYPE);
+ }
+
+ /**
+ * Returns whether the given result type is privacy safe or not.
+ */
+ public static boolean isPrivacySafe(int resultType) {
+ return (resultType & PUBLIC_DATA_TYPE) != 0;
+ }
}
diff --git a/searchuilib/src/com/android/app/search/SearchActionExtras.java b/searchuilib/src/com/android/app/search/SearchActionExtras.java
new file mode 100644
index 0000000..2f33d5d
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/SearchActionExtras.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.search;
+
+import android.app.search.SearchAction;
+
+/**
+ * Helper class that defines key string value for {@link SearchAction#getExtras()}
+ */
+public class SearchActionExtras {
+ public static final String BUNDLE_EXTRA_HIDE_SUBTITLE = "hide_subtitle";
+ public static final String BUNDLE_EXTRA_HIDE_ICON = "hide_icon";
+ public static final String BUNDLE_EXTRA_ALLOW_PINNING = "allow_pinning";
+ public static final String BUNDLE_EXTRA_BADGE_WITH_PACKAGE = "badge_with_package";
+ public static final String BUNDLE_EXTRA_PRIMARY_ICON_FROM_TITLE = "primary_icon_from_title";
+ public static final String BUNDLE_EXTRA_IS_SEARCH_IN_APP = "is_search_in_app";
+ public static final String BUNDLE_EXTRA_BADGE_WITH_COMPONENT_NAME = "badge_with_component_name";
+ public static final String BUNDLE_EXTRA_ICON_CACHE_KEY = "icon_cache_key";
+ public static final String BUNDLE_EXTRA_ICON_TOKEN_INTEGER = "icon_integer";
+ public static final String BUNDLE_EXTRA_SHOULD_START = "should_start";
+ public static final String BUNDLE_EXTRA_SHOULD_START_FOR_RESULT = "should_start_for_result";
+ public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_TEXT = "suggestion_action_text";
+ public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_RPC = "suggestion_action_rpc";
+ public static final String BUNDLE_EXTRA_SKIP_LOGGING_IN_TARGET_HANDLER =
+ "skip_logging_in_target_handler";
+}
diff --git a/searchuilib/src/com/android/app/search/SearchTargetConverter.java b/searchuilib/src/com/android/app/search/SearchTargetConverter.java
new file mode 100644
index 0000000..2080966
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/SearchTargetConverter.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.search;
+
+import static com.android.app.search.LayoutType.SMALL_ICON_HORIZONTAL_TEXT;
+import static com.android.app.search.SearchActionExtras.BUNDLE_EXTRA_HIDE_ICON;
+import static com.android.app.search.SearchActionExtras.BUNDLE_EXTRA_HIDE_SUBTITLE;
+import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_CLASS;
+import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_SUBTITLE_OVERRIDE;
+import static com.android.app.search.SearchTargetExtras.BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER;
+import static com.android.app.search.SearchTargetExtras.EXTRAS_RECENT_BLOCK_TARGET;
+
+import android.app.search.SearchAction;
+import android.app.search.SearchTarget;
+import android.content.pm.ShortcutInfo;
+import android.os.Bundle;
+
+public class SearchTargetConverter {
+ /**
+ * Generate a searchTarget that uses {@link LayoutType#SMALL_ICON_HORIZONTAL_TEXT} from a
+ * searchTarget where original layout type may not have been SMALL_ICON_HORIZONTAL_TEXT. Only
+ * possible if the given SearchTarget contains a searchAction or shortcutInfo, otherwise the
+ * original searchTarget will be returned.
+ */
+ public static SearchTarget convertLayoutTypeToSmallIconHorizontalText(
+ SearchTarget searchTarget) {
+ SearchAction searchTargetAction = searchTarget.getSearchAction();
+ ShortcutInfo shortcutInfo = searchTarget.getShortcutInfo();
+ int resultType = searchTarget.getResultType();
+ String subtitle = "";
+
+ Bundle searchTargetBundle = searchTarget.getExtras();
+ searchTargetBundle.putString(BUNDLE_EXTRA_CLASS,
+ searchTargetBundle.getString(BUNDLE_EXTRA_CLASS));
+ searchTargetBundle.putBoolean(BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER, true);
+ searchTargetBundle.putBoolean(BUNDLE_EXTRA_HIDE_SUBTITLE, false);
+ searchTargetBundle.putString(BUNDLE_EXTRA_SUBTITLE_OVERRIDE, subtitle);
+ searchTargetBundle.putBoolean(BUNDLE_EXTRA_HIDE_ICON, false);
+ searchTargetBundle.putBoolean(EXTRAS_RECENT_BLOCK_TARGET, true);
+
+ SearchTarget.Builder builder = new SearchTarget.Builder(resultType,
+ SMALL_ICON_HORIZONTAL_TEXT, searchTarget.getId())
+ .setPackageName(searchTarget.getPackageName())
+ .setExtras(searchTargetBundle)
+ .setUserHandle(searchTarget.getUserHandle());
+ if (searchTargetAction != null) {
+ builder.setSearchAction(searchTargetAction);
+ } else if (shortcutInfo != null) {
+ builder.setShortcutInfo(shortcutInfo);
+ } else {
+ return searchTarget;
+ }
+ return builder.build();
+ }
+}
diff --git a/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java b/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java
new file mode 100644
index 0000000..a323625
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/SearchTargetEventHelper.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.app.search;
+
+import static com.android.app.search.SearchTargetExtras.isRichAnswer;
+
+import android.app.search.SearchTarget;
+import android.content.ComponentName;
+import android.os.Process;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Helper class that defines helper methods for {@link android.app.search.SearchTargetEvent} to
+ * define the contract between Launcher and AiAi for notifyEvent.
+ */
+
+public class SearchTargetEventHelper {
+
+ public static final String PKG_NAME_AGSA = "com.google.android.googlequicksearchbox";
+
+ /**
+ * Generate web target id similar to AiAi targetId for logging search button tap and Launcher
+ * sends raw query to AGA.
+ * AiAi target id is of format "resultType:userId:packageName:extraInfo"
+ *
+ * @return string webTargetId
+ * Example webTargetId for
+ * web suggestion - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:SUGGESTION
+ */
+ public static String generateWebTargetIdForRawQuery() {
+ // For raw query, there is no search target, so we pass null.
+ return generateWebTargetIdForLogging(null);
+ }
+
+ /**
+ * Generate web target id similar to AiAi targetId for logging both 0-state and n-state.
+ * AiAi target id is of format "resultType:userId:packageName:extraInfo"
+ *
+ * @return string webTargetId
+ * Example webTargetId for
+ * web suggestion - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:SUGGESTION
+ * rich answer - WEB_SUGGEST:0:com.google.android.googlequicksearchbox:RICH_ANSWER
+ */
+ public static String generateWebTargetIdForLogging(@Nullable SearchTarget webTarget) {
+ StringBuilder webTargetId = new StringBuilder(
+ "WEB_SUGGEST" + ":" + Process.myUserHandle().getIdentifier() + ":");
+ if (webTarget == null) {
+ webTargetId.append(PKG_NAME_AGSA + ":SUGGESTION");
+ return webTargetId.toString();
+ }
+ webTargetId.append(webTarget.getPackageName());
+ if (isRichAnswer(webTarget)) {
+ webTargetId.append(":RICH_ANSWER");
+ } else {
+ webTargetId.append(":SUGGESTION");
+ }
+ return webTargetId.toString();
+ }
+
+ /**
+ * Generate application target id similar to AiAi targetId for logging only 0-state.
+ * For n-state, AiAi already populates the target id in right format.
+ * AiAi target id is of format "resultType:userId:packageName:extraInfo"
+ *
+ * When the apps from AiAi's AppPredictionService are converted to {@link SearchTarget}, we need
+ * to construct the targetId using componentName.
+ *
+ * @return string appTargetId
+ * Example appTargetId for
+ * maps - APPLICATION:0:com.google.android.apps.maps:com.google.android.maps.MapsActivity
+ * clock - APPLICATION:0:com.google.android.deskclock:com.android.deskclock.DeskClock
+ */
+ public static String generateAppTargetIdForLogging(@Nullable ComponentName appComponentName) {
+ StringBuilder appTargetId = new StringBuilder(
+ "APPLICATION" + ":" + Process.myUserHandle().getIdentifier() + ":");
+ if (appComponentName == null) return appTargetId.append(" : ").toString();
+ return appTargetId + appComponentName.getPackageName() + ":"
+ + appComponentName.getClassName();
+ }
+
+ /**
+ * Generate gms play target id similar to AiAi targetId for logging only n-state.
+ * AiAi target id is of format "resultType:userId:packageName:extraInfo"
+ *
+ * @return string playTargetId
+ * Example playTargetId for Candy Crush
+ * PLAY:0:com.king.candycrushsaga:Gms
+ */
+ public static String generatePlayTargetIdForLogging(String appPackage) {
+ return "PLAY" + ":" + Process.myUserHandle().getIdentifier() + ":" + appPackage + ":Gms";
+ }
+}
diff --git a/searchuilib/src/com/android/app/search/SearchTargetExtras.java b/searchuilib/src/com/android/app/search/SearchTargetExtras.java
new file mode 100644
index 0000000..887c457
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/SearchTargetExtras.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.search;
+
+import static com.android.app.search.LayoutType.TALL_CARD_WITH_IMAGE_NO_ICON;
+
+import android.app.blob.BlobHandle;
+import android.app.search.SearchAction;
+import android.app.search.SearchTarget;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Helper class that defines key string value for {@link SearchTarget#getExtras()}
+ * and also defines helper methods
+ */
+public class SearchTargetExtras {
+
+ /** on device data related extras and helper methods */
+ // Used to extra component name.
+ public static final String BUNDLE_EXTRA_CLASS = "class";
+
+ // Used for UI treatment. Labels whether search target should support quick launch.
+ public static final String BUNDLE_EXTRA_QUICK_LAUNCH = "quick_launch";
+ // Used for UI treatment. Targets grouped with same group id are decorated together.
+ public static final String BUNDLE_EXTRA_GROUP_ID = "group_id";
+ public static final String BUNDLE_EXTRA_GROUP_DECORATE_TOGETHER = "decorate_together";
+ // Used if slice title should be rendered else where outside of slice (e.g., edit text).
+ public static final String BUNDLE_EXTRA_SLICE_TITLE = "slice_title";
+ // Used if slice view should be rendered using full height mode.
+ public static final String BUNDLE_EXTRA_USE_FULL_HEIGHT = "use_full_height";
+ public static final String BUNDLE_EXTRA_IS_NON_TAPPABLE = "is_non_tappable";
+ public static final String BUNDLE_EXTRA_TITLE_OVERWRITE = "title_overwrite";
+ // Used if subtitle view should be overridden to string that is not natively defined by the
+ // search target.
+ public static final String BUNDLE_EXTRA_SUBTITLE_OVERRIDE = "subtitle_override";
+
+ // Used for logging. Returns whether spelling correction was applied.
+ public static final String BUNDLE_EXTRA_IS_QUERY_CORRECTED = "is_query_corrected";
+ // Used for logging. Returns whether the result matched block title or the inline item.
+ public static final String BUNDLE_EXTRA_RESULT_MATCH_USER_TYPED = "result_match_user_typed";
+ // Used for logging. Returns the timestamp when system service received the data.
+ public static final String BUNDLE_EXTRA_START_TIMESTAMP = "start_timestamp";
+ // Indicates the search result app location column.
+ public static final String BUNDLE_EXTRA_RESULT_APP_GRIDX = "app_gridx";
+
+ // Used for thumbnail loading. Contains handle to retrieve Blobstore asset.
+ public static final String BUNDLE_EXTRA_BLOBSTORE_HANDLE = "blobstore_handle_key";
+
+ // Used to denote this searchTarget is for recent block in 0-state.
+ public static final String EXTRAS_RECENT_BLOCK_TARGET = "recent_block_target";
+
+ public static final int GROUPING = 1 << 1;
+
+ @Nullable
+ public static String getDecoratorId(@Nullable SearchTarget target) {
+ return isTargetOrExtrasNull(target) ? null :
+ target.getExtras().getString(BUNDLE_EXTRA_GROUP_ID);
+ }
+
+ public static int getDecoratorType(@Nullable SearchTarget target) {
+ int type = 0;
+ if (isTargetOrExtrasNull(target)) {
+ return type;
+ }
+ if (!TextUtils.isEmpty(target.getExtras().getString(BUNDLE_EXTRA_GROUP_ID))) {
+ type |= GROUPING;
+ }
+ return type;
+ }
+
+ /** Whether or not the SearchTarget's Extras contains a blobstore image. */
+ public static boolean isSearchTargetBlobstoreAsset(@Nullable SearchTarget target) {
+ if (isTargetOrExtrasNull(target)) {
+ return false;
+ }
+ return target.getExtras().getParcelable(
+ BUNDLE_EXTRA_BLOBSTORE_HANDLE) instanceof BlobHandle;
+ }
+
+ /** Check if SearchTarget contains information to tell if this target is from recent block. */
+ public static boolean isSearchTargetRecentItem(@Nullable SearchTarget target) {
+ if (isTargetOrExtrasNull(target)) {
+ return false;
+ }
+ return target.getExtras().getBoolean(EXTRAS_RECENT_BLOCK_TARGET, false);
+ }
+
+ private static boolean isTargetOrExtrasNull(@Nullable SearchTarget target) {
+ return target == null || target.getExtras() == null;
+ }
+
+ /** Web data related extras and helper methods */
+ public static final String BUNDLE_EXTRA_PROXY_WEB_ITEM = "proxy_web_item";
+ public static final String BUNDLE_EXTRA_ENTITY = "is_entity";
+ public static final String BUNDLE_EXTRA_ANSWER = "is_answer";
+ public static final String BUNDLE_EXTRA_RESPONSE_ID = "response_id";
+ public static final String BUNDLE_EXTRA_LEARN_MORE_URL = "learn_more_url";
+ public static final String BUNDLE_EXTRA_PERSONAL = "is_personal";
+ public static final String BUNDLE_EXTRA_SUGGESTION_TYPE = "suggestion_type";
+ public static final String BUNDLE_EXTRA_SUGGEST_RENDER_TEXT = "suggest_render_text";
+ public static final String BUNDLE_EXTRA_ZERO_STATE_CACHE = "zero_state_cache";
+ public static final String BUNDLE_EXTRA_TALL_CARD_HEADER = "tall_card_header";
+ public static final String BUNDLE_EXTRA_TALL_CARD_IMAGE_DESCRIPTION =
+ "tall_card_image_description";
+ public static final String BUNDLE_EXTRA_BITMAP_URL = "bitmap_url";
+
+ // Used for web suggestions count for both AA+ and QSB entry point.
+ // Returns the number of web suggestions to be shown.
+ public static final String WEB_SUG_COUNT = "web_sug_count";
+
+ /**
+ * Replaced with thumbnail crop type
+ *
+ * Flag to control whether thumbnail(s) should fill the thumbnail container's width or not.
+ * When this flag is true, when there are less than the maximum number of thumbnails in the
+ * container, the thumbnails will stretch to fill the container's width.
+ * When this flag is false, thumbnails will always be cropped to a square ratio even if
+ * there aren't enough thumbnails to fill the container.
+ *
+ * Only relevant in {@link LayoutType#THUMBNAIL_CONTAINER} and {@link LayoutType#THUMBNAIL}.
+ */
+ @Deprecated
+ public static final String BUNDLE_EXTRA_SHOULD_FILL_CONTAINER_WIDTH =
+ "should_fill_container_width";
+
+ /**
+ * Flag to control thumbnail container's crop mode, controlling the layout
+ *
+ * <ul>
+ * <li>SQUARE: Thumbnail(s) will be cropped to a square aspect ratio around the center.</li>
+ * <li>FILL_WIDTH: Thumbnail(s) should collectively fill the thumbnail container's width.
+ * When there are less than the maximum number of thumbnails in the container, the
+ * layouts' width will stretch to fit the container, the images will fill the width
+ * and then the top/bottom cropped to fit.</li>
+ * <li>FILL_HEIGHT: Thumbnail(s) should fill height and be cropped to fit in the width
+ * based on {@link BUNDLE_EXTRA_THUMBNAIL_MAX_COUNT} as the column count. When the image
+ * width is larger than the width / column, both sides will be cropped while maintaining
+ * the center.
+ * When there are less thumbnails than the max count, the layout will be constrained to
+ * equally divide the width of the container. If there are more thumbnails than the max
+ * count, the excessive thumbnails will be ignored.</li>
+ * </ul>
+ *
+ * Only relevant in {@link LayoutType#THUMBNAIL_CONTAINER} and {@link LayoutType#THUMBNAIL}.
+ */
+ public static final String BUNDLE_EXTRA_THUMBNAIL_CROP_TYPE = "thumbnail_crop_type";
+ public enum ThumbnailCropType {
+ DEFAULT(0), // defaults to SQUARE behavior by {@link LayoutType#THUMBNAIL_CONTAINER}.
+ SQUARE(1),
+ FILL_WIDTH(2),
+ FILL_HEIGHT(3);
+
+ private final int mTypeId;
+
+ ThumbnailCropType(int typeId) {
+ mTypeId = typeId;
+ }
+
+ public int toTypeId() {
+ return mTypeId;
+ }
+ };
+
+ /**
+ * How many grid spaces for the thumbnail container should be reserved.
+ * Only relevant for {@link ThumbnailCropType#FILL_HEIGHT} crop type.
+ */
+ public static final String BUNDLE_EXTRA_THUMBNAIL_MAX_COUNT = "thumbnail_max_count";
+
+ /**
+ * Flag to control whether the SearchTarget's label should be hidden.
+ * When this flag is true, label will be hidden.
+ * When this flag is false (or omitted), {@link SearchAction#mTitle} will be shown.
+ */
+ public static final String BUNDLE_EXTRA_HIDE_LABEL =
+ "hide_label";
+ public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_TEXT = "suggestion_action_text";
+ public static final String BUNDLE_EXTRA_SUGGESTION_ACTION_RPC = "suggestion_action_rpc";
+ public static final String BUNDLE_EXTRA_SUPPORT_QUERY_BUILDER = "support_query_builder";
+ public static final String BUNDLE_EXTRA_SUGGEST_RAW_TEXT = "suggest_raw_text";
+ public static final String BUNDLE_EXTRA_SUGGEST_TRUNCATE_START = "suggest_truncate_start";
+
+ /** Web data related helper methods */
+ public static boolean isEntity(@Nullable SearchTarget target) {
+ return target != null && target.getExtras() != null
+ && target.getExtras().getBoolean(BUNDLE_EXTRA_ENTITY);
+ }
+
+ public static boolean isAnswer(@Nullable SearchTarget target) {
+ return target != null && target.getExtras() != null
+ && target.getExtras().getBoolean(BUNDLE_EXTRA_ANSWER);
+ }
+
+ /** Whether the search target is a rich answer web result. */
+ public static boolean isRichAnswer(@Nullable SearchTarget target) {
+ return target != null && isAnswer(target)
+ && target.getLayoutType().equals(TALL_CARD_WITH_IMAGE_NO_ICON);
+ }
+
+ /** Get the crop type thumbnails should use. Returns DEFAULT if not specified. */
+ public static ThumbnailCropType getThumbnailCropType(@Nullable SearchTarget target)
+ throws ArrayIndexOutOfBoundsException {
+ Bundle extras = target == null ? Bundle.EMPTY : target.getExtras();
+ if (extras.isEmpty()) {
+ return ThumbnailCropType.DEFAULT;
+ }
+ ThumbnailCropType cropType = ThumbnailCropType.values()[extras.getInt(
+ BUNDLE_EXTRA_THUMBNAIL_CROP_TYPE)];
+ return cropType != null ? cropType : ThumbnailCropType.DEFAULT;
+ }
+}
diff --git a/searchuilib/src/com/android/app/search/SearchTargetGenerator.java b/searchuilib/src/com/android/app/search/SearchTargetGenerator.java
new file mode 100644
index 0000000..22e5a86
--- /dev/null
+++ b/searchuilib/src/com/android/app/search/SearchTargetGenerator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.app.search;
+
+import static com.android.app.search.LayoutType.EMPTY_DIVIDER;
+import static com.android.app.search.LayoutType.SECTION_HEADER;
+import static com.android.app.search.ResultType.NO_FULFILLMENT;
+
+import android.app.search.SearchTarget;
+import android.os.Bundle;
+import android.os.Process;
+import android.os.UserHandle;
+
+public class SearchTargetGenerator {
+ private static final UserHandle USERHANDLE = Process.myUserHandle();
+
+ public static SearchTarget EMPTY_DIVIDER_TARGET =
+ new SearchTarget.Builder(NO_FULFILLMENT, EMPTY_DIVIDER, "divider")
+ .setPackageName("") /* required but not used*/
+ .setUserHandle(USERHANDLE) /* required */
+ .setExtras(new Bundle())
+ .build();
+
+ public static SearchTarget SECTION_HEADER_TARGET =
+ new SearchTarget.Builder(NO_FULFILLMENT, SECTION_HEADER, "section_header")
+ .setPackageName("") /* required but not used*/
+ .setUserHandle(USERHANDLE) /* required */
+ .setExtras(new Bundle())
+ .build();
+}
diff --git a/viewcapturelib/Android.bp b/viewcapturelib/Android.bp
index d5700e5..33da2dd 100644
--- a/viewcapturelib/Android.bp
+++ b/viewcapturelib/Android.bp
@@ -20,12 +20,12 @@ java_library {
name: "view_capture_proto",
srcs: ["src/com/android/app/viewcapture/proto/*.proto"],
proto: {
- type: "nano",
+ type: "lite",
local_include_dirs:[
"src/com/android/app/viewcapture/proto"
],
},
- static_libs: ["libprotobuf-java-nano"],
+ static_libs: ["libprotobuf-java-lite"],
java_version: "1.8",
}
diff --git a/viewcapturelib/AndroidManifest.xml b/viewcapturelib/AndroidManifest.xml
index e5127c6..1da8129 100644
--- a/viewcapturelib/AndroidManifest.xml
+++ b/viewcapturelib/AndroidManifest.xml
@@ -16,5 +16,8 @@
-->
<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/build.gradle b/viewcapturelib/build.gradle
index 7f56819..3f40ad6 100644
--- a/viewcapturelib/build.gradle
+++ b/viewcapturelib/build.gradle
@@ -35,7 +35,7 @@ android {
dependencies {
implementation "androidx.core:core:1.9.0"
- implementation PROTOBUF_DEPENDENCY
+ implementation "com.google.protobuf:protobuf-lite:${protobuf_version}"
androidTestImplementation project(':SharedTestLib')
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation "androidx.test:rules:1.4.0"
@@ -45,15 +45,21 @@ protobuf {
// Configure the protoc executable
protoc {
artifact = "com.google.protobuf:protoc:${protobuf_version}${PROTO_ARCH_SUFFIX}"
- generateProtoTasks {
- all().each { task ->
- task.builtins {
- remove java
- javanano {
- option "enum_style=c"
- }
- }
+ }
+ 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
index 1a18193..e3450f6 100644
--- a/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
+++ b/viewcapturelib/src/com/android/app/viewcapture/LooperExecutor.java
@@ -28,7 +28,7 @@ import java.util.concurrent.RunnableFuture;
/**
* Implementation of {@link Executor} which executes on a provided looper.
*/
-class LooperExecutor implements Executor {
+public class LooperExecutor implements Executor {
private final Handler mHandler;
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..c84d4d5
--- /dev/null
+++ b/viewcapturelib/src/com/android/app/viewcapture/SettingsAwareViewCapture.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.database.ContentObserver
+import android.os.Handler
+import android.os.Looper
+import android.os.Process
+import android.provider.Settings
+import android.view.Choreographer
+import androidx.annotation.AnyThread
+import androidx.annotation.VisibleForTesting
+import java.util.concurrent.Executor
+
+/**
+ * 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) {
+
+ 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)
+ }
+ }
+ }
+
+ 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/ViewCapture.java b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
index 70c58cb..fcd7ad8 100644
--- a/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
+++ b/viewcapturelib/src/com/android/app/viewcapture/ViewCapture.java
@@ -35,18 +35,14 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
-import android.os.Process;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
-import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
-import com.android.app.viewcapture.data.nano.ExportedData;
-import com.android.app.viewcapture.data.nano.FrameData;
-import com.android.app.viewcapture.data.nano.ViewNode;
-
-import com.google.protobuf.nano.MessageNano;
+import com.android.app.viewcapture.data.ExportedData;
+import com.android.app.viewcapture.data.FrameData;
+import com.android.app.viewcapture.data.ViewNode;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
@@ -54,9 +50,8 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executor;
import java.util.Optional;
+import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.function.Consumer;
import java.util.zip.GZIPOutputStream;
@@ -64,7 +59,7 @@ import java.util.zip.GZIPOutputStream;
/**
* Utility class for capturing view data every frame
*/
-public class ViewCapture {
+public abstract class ViewCapture {
private static final String TAG = "ViewCapture";
@@ -74,55 +69,31 @@ public class ViewCapture {
// Number of frames to keep in memory
private final int mMemorySize;
- private static final int DEFAULT_MEMORY_SIZE = 2000;
+ 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.
- private static final int DEFAULT_INIT_POOL_SIZE = 300;
+ protected static final int DEFAULT_INIT_POOL_SIZE = 300;
- private static ViewCapture INSTANCE;
public static final LooperExecutor MAIN_EXECUTOR = new LooperExecutor(Looper.getMainLooper());
- public static ViewCapture getInstance() {
- return getInstance(true, DEFAULT_MEMORY_SIZE, DEFAULT_INIT_POOL_SIZE);
- }
-
- @VisibleForTesting
- public static ViewCapture getInstance(boolean offloadToBackgroundThread, int memorySize,
- int initPoolSize) {
- if (INSTANCE == null) {
- if (Looper.myLooper() == Looper.getMainLooper()) {
- INSTANCE = new ViewCapture(offloadToBackgroundThread, memorySize, initPoolSize);
- } else {
- try {
- return MAIN_EXECUTOR.submit(() ->
- getInstance(offloadToBackgroundThread, memorySize, initPoolSize)).get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
- }
- }
- return INSTANCE;
- }
-
private final List<WindowListener> mListeners = new ArrayList<>();
- private final Executor mExecutor;
+ 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;
- private ViewCapture(boolean offloadToBackgroundThread, int memorySize, int initPoolSize) {
+ protected ViewCapture(int memorySize, int initPoolSize, Choreographer choreographer,
+ Executor bgExecutor) {
mMemorySize = memorySize;
- if (offloadToBackgroundThread) {
- mExecutor = createAndStartNewLooperExecutor("ViewCapture",
- Process.THREAD_PRIORITY_FOREGROUND);
- } else {
- mExecutor = MAIN_EXECUTOR;
- }
- mExecutor.execute(() -> initPool(initPoolSize));
+ mChoreographer = choreographer;
+ mBgExecutor = bgExecutor;
+ mBgExecutor.execute(() -> initPool(initPoolSize));
}
- private static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
+ public static LooperExecutor createAndStartNewLooperExecutor(String name, int priority) {
HandlerThread thread = new HandlerThread(name, priority);
thread.start();
return new LooperExecutor(thread.getLooper());
@@ -158,22 +129,34 @@ public class ViewCapture {
}
/**
- * Attaches the ViewCapture to the provided window and returns a handle to detach the listener
+ * 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.
*/
public SafeCloseable startCapture(View view, String name) {
WindowListener listener = new WindowListener(view, name);
- mExecutor.execute(() -> MAIN_EXECUTOR.execute(listener::attachToRoot));
+ if (mIsEnabled) MAIN_EXECUTOR.execute(listener::attachToRoot);
mListeners.add(listener);
return () -> {
mListeners.remove(listener);
- listener.destroy();
+ listener.detachFromRoot();
};
}
+ @UiThread
+ protected void enableOrDisableWindowListeners(boolean isEnabled) {
+ mIsEnabled = isEnabled;
+ mListeners.forEach(WindowListener::detachFromRoot);
+ if (mIsEnabled) mListeners.forEach(WindowListener::attachToRoot);
+ }
+
+
/**
* Dumps all the active view captures
*/
public void dump(PrintWriter writer, FileDescriptor out, Context context) {
+ if (!mIsEnabled) {
+ return;
+ }
ViewIdProvider idProvider = new ViewIdProvider(context.getResources());
// Collect all the tasks first so that all the tasks are posted on the executor
@@ -181,7 +164,7 @@ public class ViewCapture {
.map(l -> {
FutureTask<ExportedData> task =
new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider));
- mExecutor.execute(task);
+ mBgExecutor.execute(task);
return Pair.create(l.name, task);
})
.collect(toList());
@@ -196,7 +179,7 @@ public class ViewCapture {
ExportedData data = pair.second.get();
OutputStream encodedOS = new GZIPOutputStream(new Base64OutputStream(os,
Base64.NO_CLOSE | Base64.NO_PADDING | Base64.NO_WRAP));
- encodedOS.write(MessageNano.toByteArray(data));
+ data.writeTo(encodedOS);
encodedOS.close();
os.flush();
} catch (Exception e) {
@@ -216,7 +199,7 @@ public class ViewCapture {
.map(l -> {
FutureTask<ExportedData> task =
new FutureTask<ExportedData>(() -> l.dumpToProto(idProvider));
- mExecutor.execute(task);
+ mBgExecutor.execute(task);
return task;
})
.findFirst();
@@ -272,16 +255,10 @@ public class ViewCapture {
private final long[] mFrameTimesNanosBg = new long[mMemorySize];
private final ViewPropertyRef[] mNodesBg = new ViewPropertyRef[mMemorySize];
- private boolean mDestroyed = false;
+ private boolean mIsActive = true;
private final Consumer<ViewRef> mCaptureCallback = this::captureViewPropertiesBg;
- private Choreographer mChoreographer;
WindowListener(View view, String name) {
- try {
- mChoreographer = MAIN_EXECUTOR.submit(Choreographer::getInstance).get();
- } catch (InterruptedException | ExecutionException e) {
- throw new RuntimeException(e);
- }
mRoot = view;
this.name = name;
}
@@ -300,7 +277,7 @@ public class ViewCapture {
if (captured != null) {
captured.callback = mCaptureCallback;
captured.choreographerTimeNanos = mChoreographer.getFrameTimeNanos();
- mExecutor.execute(captured);
+ mBgExecutor.execute(captured);
}
mIsFirstFrame = false;
Trace.endSection();
@@ -392,14 +369,15 @@ public class ViewCapture {
}
void attachToRoot() {
+ mIsActive = true;
if (mRoot.isAttachedToWindow()) {
- mRoot.getViewTreeObserver().addOnDrawListener(this);
+ safelyEnableOnDrawListener();
} else {
mRoot.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
- if (!mDestroyed) {
- mRoot.getViewTreeObserver().addOnDrawListener(WindowListener.this);
+ if (mIsActive) {
+ safelyEnableOnDrawListener();
}
mRoot.removeOnAttachStateChangeListener(this);
}
@@ -411,29 +389,34 @@ public class ViewCapture {
}
}
- void destroy() {
+ void detachFromRoot() {
+ mIsActive = false;
mRoot.getViewTreeObserver().removeOnDrawListener(this);
- mDestroyed = true;
+ }
+
+ private void safelyEnableOnDrawListener() {
+ mRoot.getViewTreeObserver().removeOnDrawListener(this);
+ mRoot.getViewTreeObserver().addOnDrawListener(this);
}
@WorkerThread
private ExportedData dumpToProto(ViewIdProvider idProvider) {
int size = (mNodesBg[mMemorySize - 1] == null) ? mFrameIndexBg + 1 : mMemorySize;
- ExportedData exportedData = new ExportedData();
- exportedData.frameData = new FrameData[size];
+ ExportedData.Builder exportedDataBuilder = ExportedData.newBuilder();
ArrayList<Class> classList = new ArrayList<>();
for (int i = size - 1; i >= 0; i--) {
int index = (mMemorySize + mFrameIndexBg - i) % mMemorySize;
- ViewNode node = new ViewNode();
- mNodesBg[index].toProto(idProvider, classList, node);
- FrameData frameData = new FrameData();
- frameData.node = node;
- frameData.timestamp = mFrameTimesNanosBg[index];
- exportedData.frameData[size - i - 1] = frameData;
+ ViewNode.Builder nodeBuilder = ViewNode.newBuilder();
+ mNodesBg[index].toProto(idProvider, classList, nodeBuilder);
+ FrameData.Builder frameDataBuilder = FrameData.newBuilder()
+ .setNode(nodeBuilder)
+ .setTimestamp(mFrameTimesNanosBg[index]);
+ exportedDataBuilder.addFrameData(frameDataBuilder);
}
- exportedData.classname = classList.stream().map(Class::getName).toArray(String[]::new);
- return exportedData;
+ return exportedDataBuilder
+ .addAllClassname(classList.stream().map(Class::getName).collect(toList()))
+ .build();
}
private ViewRef captureViewTree(View view, ViewRef start) {
@@ -518,35 +501,35 @@ public class ViewCapture {
* at the end of the iteration.
*/
public ViewPropertyRef toProto(ViewIdProvider idProvider, ArrayList<Class> classList,
- ViewNode viewNode) {
+ ViewNode.Builder viewNode) {
int classnameIndex = classList.indexOf(clazz);
if (classnameIndex < 0) {
classnameIndex = classList.size();
classList.add(clazz);
}
- viewNode.classnameIndex = classnameIndex;
- viewNode.hashcode = hashCode;
- viewNode.id = idProvider.getName(id);
- viewNode.left = left;
- viewNode.top = top;
- viewNode.width = right - left;
- viewNode.height = bottom - top;
- viewNode.translationX = translateX;
- viewNode.translationY = translateY;
- viewNode.scaleX = scaleX;
- viewNode.scaleY = scaleY;
- viewNode.alpha = alpha;
- viewNode.visibility = visibility;
- viewNode.willNotDraw = willNotDraw;
- viewNode.elevation = elevation;
- viewNode.clipChildren = clipChildren;
+
+ viewNode.setClassnameIndex(classnameIndex)
+ .setHashcode(hashCode)
+ .setId(idProvider.getName(id))
+ .setLeft(left)
+ .setTop(top)
+ .setWidth(right - left)
+ .setHeight(bottom - top)
+ .setTranslationX(translateX)
+ .setTranslationY(translateY)
+ .setScaleX(scaleX)
+ .setScaleY(scaleY)
+ .setAlpha(alpha)
+ .setVisibility(visibility)
+ .setWillNotDraw(willNotDraw)
+ .setElevation(elevation)
+ .setClipChildren(clipChildren);
ViewPropertyRef result = next;
- viewNode.children = new ViewNode[childCount];
for (int i = 0; (i < childCount) && (result != null); i++) {
- ViewNode childViewNode = new ViewNode();
+ ViewNode.Builder childViewNode = ViewNode.newBuilder();
result = result.toProto(idProvider, classList, childViewNode);
- viewNode.children[i] = childViewNode;
+ viewNode.addChildren(childViewNode);
}
return result;
}
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..e08b549
--- /dev/null
+++ b/viewcapturelib/tests/com/android/app/viewcapture/SettingsAwareViewCaptureTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.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 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)
+
+ @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
index d390c72..b0fcca1 100644
--- a/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
+++ b/viewcapturelib/tests/com/android/app/viewcapture/ViewCaptureTest.kt
@@ -16,11 +16,10 @@
package com.android.app.viewcapture
-import android.app.Activity
import android.content.Intent
import android.media.permission.SafeCloseable
-import android.os.Bundle
import android.testing.AndroidTestingRunner
+import android.view.Choreographer
import android.view.View
import android.widget.LinearLayout
import android.widget.TextView
@@ -28,7 +27,7 @@ import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.app.viewcapture.TestActivity.Companion.TEXT_VIEW_COUNT
-import com.android.app.viewcapture.data.nano.ExportedData
+import com.android.app.viewcapture.data.ExportedData
import junit.framework.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
@@ -38,106 +37,80 @@ import org.junit.runner.RunWith
@RunWith(AndroidTestingRunner::class)
class ViewCaptureTest {
- private val viewCaptureMemorySize = 100
- private val viewCaptureInitPoolSize = 15
- private val viewCapture =
- ViewCapture.getInstance(false, viewCaptureMemorySize, viewCaptureInitPoolSize)
+ 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)
-
+ @get:Rule val activityScenarioRule = ActivityScenarioRule<TestActivity>(activityIntent)
@Test
fun testViewCaptureDumpsOneFrameAfterInvalidate() {
- val closeable = startViewCaptureAndInvalidateNTimes(1)
-
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- val exportedData = viewCapture.getDumpTask(rootView).get().get()
-
- assertEquals(1, exportedData.frameData.size)
- verifyTestActivityViewHierarchy(exportedData)
+ activityScenarioRule.scenario.onActivity { activity ->
+ Choreographer.getInstance().postFrameCallback {
+ val closeable = startViewCaptureAndInvalidateNTimes(1, activity)
+ val rootView = activity.findViewById<View>(android.R.id.content)
+ val exportedData = viewCapture.getDumpTask(rootView).get().get()
+
+ assertEquals(1, exportedData.frameDataList.size)
+ verifyTestActivityViewHierarchy(exportedData)
+ closeable.close()
+ }
}
- closeable?.close()
}
@Test
fun testViewCaptureDumpsCorrectlyAfterRecyclingStarted() {
- val closeable = startViewCaptureAndInvalidateNTimes(viewCaptureMemorySize + 5)
-
- // waits until main looper has no remaining tasks and is idle
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- val exportedData = 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(viewCaptureMemorySize, exportedData.frameData.size)
- verifyTestActivityViewHierarchy(exportedData)
+ activityScenarioRule.scenario.onActivity { activity ->
+ Choreographer.getInstance().postFrameCallback {
+ val closeable = startViewCaptureAndInvalidateNTimes(memorySize + 5, activity)
+ val rootView = activity.findViewById<View>(android.R.id.content)
+ val exportedData = viewCapture.getDumpTask(rootView).get().get()
+
+ // since ViewCapture MEMORY_SIZE is [viewCaptureMemorySize], only
+ // [viewCaptureMemorySize] frames are exported, although the view is invalidated
+ // [viewCaptureMemorySize + 5] times
+ assertEquals(memorySize, exportedData.frameDataList.size)
+ verifyTestActivityViewHierarchy(exportedData)
+ closeable.close()
+ }
}
- closeable?.close()
}
- private fun startViewCaptureAndInvalidateNTimes(n: Int): SafeCloseable? {
- var closeable: SafeCloseable? = null
- activityScenarioRule.scenario.onActivity {
- val rootView = it.findViewById<View>(android.R.id.content)
- closeable = viewCapture.startCapture(rootView, "rootViewId")
- invalidateView(rootView, times = n)
- }
+ 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 invalidateView(view: View, times: Int) {
- if (times <= 0) return
- view.post {
- view.invalidate()
- invalidateView(view, times - 1)
+ private fun dispatchOnDraw(view: View, times: Int) {
+ if (times > 0) {
+ view.viewTreeObserver.dispatchOnDraw()
+ dispatchOnDraw(view, times - 1)
}
}
private fun verifyTestActivityViewHierarchy(exportedData: ExportedData) {
- val classnames = exportedData.classname
- for (frame in exportedData.frameData) {
- val root = frame.node // FrameLayout (android.R.id.content)
- val testActivityRoot = root.children.first() // LinearLayout (set by setContentView())
- assertEquals(TEXT_VIEW_COUNT, testActivityRoot.children.size)
+ 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,
- classnames[testActivityRoot.classnameIndex]
+ exportedData.getClassname(testActivityRoot.classnameIndex)
)
assertEquals(
TextView::class.qualifiedName,
- classnames[testActivityRoot.children.first().classnameIndex]
+ exportedData.getClassname(testActivityRoot.childrenList.first().classnameIndex)
)
}
}
}
-
-/**
- * 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
- }
-}