diff options
| author | George Zacharia <george.zcharia@gmail.com> | 2023-07-02 15:00:14 +0530 |
|---|---|---|
| committer | George Zacharia <george.zcharia@gmail.com> | 2023-07-02 15:00:14 +0530 |
| commit | e1fe4bad8afa333499590808f3994c8bea0c833b (patch) | |
| tree | 84db2665b4d6f4e0ae52cecf10eef27facf72cb5 | |
| parent | 2d42f0cd9b97e656643049457e668864063a7051 (diff) | |
| parent | 313ca2b3cbaef97cb4665b24412eccf3d4cdcae3 (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
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 - } -} |
