From 20e622725e112c85edd3fc7f52592782bf63b9a2 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Wed, 19 Apr 2023 09:18:56 -0700 Subject: Load shortucts concurrently with app target resolution Reload shortcuts whenever we rebuild the target list: * in ChooserActivity#onHandlePackageChange(), called by system package change broadcast, target pinning, and ResolverActivity#onRestart; * in work profile status broadcast receiver. Tie ShorctuLoader to a Lifecycle (instead of exposing lifecycle methods i.e. destroy) and switch to coroutines from executors. Add a startup-to-first-shortcut tracing and a debug log. Bug: 262927266 Test: manual testing, unit tests. Change-Id: Iaa4bd9a88f29378d75d88b2ea8fc3698cbd3be8f --- .../android/intentresolver/ChooserActivity.java | 54 +++++--- .../android/intentresolver/ResolverActivity.java | 18 +-- .../android/intentresolver/measurements/Tracer.kt | 46 +++++++ .../intentresolver/shortcuts/ShortcutLoader.kt | 150 ++++++++++++--------- 4 files changed, 182 insertions(+), 86 deletions(-) create mode 100644 java/src/com/android/intentresolver/measurements/Tracer.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 97161452..7f55f78f 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -88,6 +88,7 @@ import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.measurements.Tracer; import com.android.intentresolver.model.AbstractResolverComparator; import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; @@ -227,6 +228,7 @@ public class ChooserActivity extends ResolverActivity implements @Override protected void onCreate(Bundle savedInstanceState) { + Tracer.INSTANCE.markLaunched(); final long intentReceivedTime = System.currentTimeMillis(); mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET); @@ -362,7 +364,10 @@ public class ChooserActivity extends ResolverActivity implements private void createProfileRecords( AppPredictorFactory factory, IntentFilter targetIntentFilter) { UserHandle mainUserHandle = getPersonalProfileUserHandle(); - createProfileRecord(mainUserHandle, targetIntentFilter, factory); + ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); + if (record.shortcutLoader == null) { + Tracer.INSTANCE.endLaunchToShortcutTrace(); + } UserHandle workUserHandle = getWorkProfileUserHandle(); if (workUserHandle != null) { @@ -370,7 +375,7 @@ public class ChooserActivity extends ResolverActivity implements } } - private void createProfileRecord( + private ProfileRecord createProfileRecord( UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) { AppPredictor appPredictor = factory.create(userHandle); ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic() @@ -381,9 +386,9 @@ public class ChooserActivity extends ResolverActivity implements userHandle, targetIntentFilter, shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult)); - mProfileRecords.put( - userHandle.getIdentifier(), - new ProfileRecord(appPredictor, shortcutLoader)); + ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader); + mProfileRecords.put(userHandle.getIdentifier(), record); + return record; } @Nullable @@ -400,6 +405,7 @@ public class ChooserActivity extends ResolverActivity implements Consumer callback) { return new ShortcutLoader( context, + getLifecycle(), appPredictor, userHandle, targetIntentFilter, @@ -580,16 +586,25 @@ public class ChooserActivity extends ResolverActivity implements // Refresh pinned items mPinnedSharedPrefs = getPinnedSharedPrefs(this); if (listAdapter == null) { - mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); + handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter()); if (mChooserMultiProfilePagerAdapter.getCount() > 1) { - mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged(); + handlePackageChangePerProfile( + mChooserMultiProfilePagerAdapter.getInactiveListAdapter()); } } else { - listAdapter.handlePackagesChanged(); + handlePackageChangePerProfile(listAdapter); } updateProfileViewButton(); } + private void handlePackageChangePerProfile(ResolverListAdapter adapter) { + ProfileRecord record = getProfileRecord(adapter.getUserHandle()); + if (record != null && record.shortcutLoader != null) { + record.shortcutLoader.reset(); + } + adapter.handlePackagesChanged(); + } + @Override protected void onResume() { super.onResume(); @@ -1254,6 +1269,16 @@ public class ChooserActivity extends ResolverActivity implements initialIntentsUserSpace); } + @Override + protected void onWorkProfileStatusUpdated() { + UserHandle workUser = getWorkProfileUserHandle(); + ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); + if (record != null && record.shortcutLoader != null) { + record.shortcutLoader.reset(); + } + super.onWorkProfileStatusUpdated(); + } + @Override @VisibleForTesting protected ChooserListController createListController(UserHandle userHandle) { @@ -1520,14 +1545,11 @@ public class ChooserActivity extends ResolverActivity implements private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) { UserHandle userHandle = chooserListAdapter.getUserHandle(); ProfileRecord record = getProfileRecord(userHandle); - if (record == null) { - return; - } - if (record.shortcutLoader == null) { + if (record == null || record.shortcutLoader == null) { return; } record.loadingStartTime = SystemClock.elapsedRealtime(); - record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos()); + record.shortcutLoader.updateAppTargets(chooserListAdapter.getDisplayResolveInfos()); } @MainThread @@ -1553,6 +1575,9 @@ public class ChooserActivity extends ResolverActivity implements adapter.completeServiceTargetLoading(); } + if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { + Tracer.INSTANCE.endLaunchToShortcutTrace(); + } logDirectShareTargetReceived(userHandle); sendVoiceChoicesIfNeeded(); getChooserActivityLogger().logSharesheetDirectLoadComplete(); @@ -1883,9 +1908,6 @@ public class ChooserActivity extends ResolverActivity implements } public void destroy() { - if (shortcutLoader != null) { - shortcutLoader.destroy(); - } if (appPredictor != null) { appPredictor.destroy(); } diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 66eae92d..aea6c2c9 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -1003,21 +1003,21 @@ public class ResolverActivity extends FragmentActivity implements return new CrossProfileIntentsChecker(getContentResolver()); } - // @NonFinalForTesting - @VisibleForTesting protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() { final UserHandle workUser = getWorkProfileUserHandle(); return new WorkProfileAvailabilityManager( getSystemService(UserManager.class), workUser, - () -> { - if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(workUser)) { - mMultiProfilePagerAdapter.rebuildActiveTab(true); - } else { - mMultiProfilePagerAdapter.clearInactiveProfileCache(); - } - }); + this::onWorkProfileStatusUpdated); + } + + protected void onWorkProfileStatusUpdated() { + if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getWorkProfileUserHandle())) { + mMultiProfilePagerAdapter.rebuildActiveTab(true); + } else { + mMultiProfilePagerAdapter.clearInactiveProfileCache(); + } } // @NonFinalForTesting diff --git a/java/src/com/android/intentresolver/measurements/Tracer.kt b/java/src/com/android/intentresolver/measurements/Tracer.kt new file mode 100644 index 00000000..168bda0e --- /dev/null +++ b/java/src/com/android/intentresolver/measurements/Tracer.kt @@ -0,0 +1,46 @@ +/* + * 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.intentresolver.measurements + +import android.os.Trace +import android.os.SystemClock +import android.util.Log +import java.util.concurrent.atomic.AtomicLong + +private const val TAG = "Tracer" +private const val SECTION_LAUNCH_TO_SHORTCUT = "launch-to-shortcut" + +object Tracer { + private val launchToFirstShortcut = AtomicLong(-1L) + + fun markLaunched() { + if (launchToFirstShortcut.compareAndSet(-1, elapsedTimeNow())) { + Trace.beginAsyncSection(SECTION_LAUNCH_TO_SHORTCUT, 1) + } + } + + fun endLaunchToShortcutTrace() { + val time = elapsedTimeNow() + val startTime = launchToFirstShortcut.get() + if (startTime >= 0 && launchToFirstShortcut.compareAndSet(startTime, -1L)) { + Trace.endAsyncSection(SECTION_LAUNCH_TO_SHORTCUT, 1) + Log.d(TAG, "stat to first shortcut time: ${time - startTime} ms") + } + } + + private fun elapsedTimeNow() = SystemClock.elapsedRealtime() +} diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt index 29e706d4..ee6893d0 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -26,7 +26,6 @@ import android.content.pm.PackageManager import android.content.pm.ShortcutInfo import android.content.pm.ShortcutManager import android.content.pm.ShortcutManager.ShareShortcutInfo -import android.os.AsyncTask import android.os.UserHandle import android.os.UserManager import android.service.chooser.ChooserTarget @@ -36,12 +35,19 @@ import androidx.annotation.MainThread import androidx.annotation.OpenForTesting import androidx.annotation.VisibleForTesting import androidx.annotation.WorkerThread +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope import com.android.intentresolver.chooser.DisplayResolveInfo -import java.lang.RuntimeException -import java.util.ArrayList -import java.util.HashMap +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch import java.util.concurrent.Executor -import java.util.concurrent.atomic.AtomicReference import java.util.function.Consumer /** @@ -49,86 +55,107 @@ import java.util.function.Consumer * * * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut - * updates. The shortcut loading is triggered by the [queryShortcuts], - * the processing will happen on the [backgroundExecutor] and the result is delivered - * through the [callback] on the [callbackExecutor], the main thread. - * - * - * The current version does not improve on the legacy in a way that it does not guarantee that - * each invocation of the [queryShortcuts] will be matched by an - * invocation of the callback (there are early terminations of the flow). Also, the fetched - * shortcuts would be matched against the last known input, i.e. two invocations of - * [queryShortcuts] may result in two callbacks where shortcuts are - * processed against the latest input. - * + * updates. The shortcut loading is triggered in the constructor or by the [reset] method, + * the processing happens on the [dispatcher] and the result is delivered + * through the [callback] on the default [lifecycle]'s dispatcher, the main thread. */ @OpenForTesting open class ShortcutLoader @VisibleForTesting constructor( private val context: Context, + private val lifecycle: Lifecycle, private val appPredictor: AppPredictorProxy?, private val userHandle: UserHandle, private val isPersonalProfile: Boolean, private val targetIntentFilter: IntentFilter?, - private val backgroundExecutor: Executor, - private val callbackExecutor: Executor, + private val dispatcher: CoroutineDispatcher, private val callback: Consumer ) { private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter() private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager - private val activeRequest = AtomicReference(NO_REQUEST) private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) } - @Volatile - private var isDestroyed = false + private val appTargetSource = MutableSharedFlow?>( + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + private val shortcutSource = MutableSharedFlow( + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + private val isDestroyed get() = !lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED) @MainThread constructor( context: Context, + lifecycle: Lifecycle, appPredictor: AppPredictor?, userHandle: UserHandle, targetIntentFilter: IntentFilter?, callback: Consumer ) : this( context, + lifecycle, appPredictor?.let { AppPredictorProxy(it) }, userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()), targetIntentFilter, - AsyncTask.SERIAL_EXECUTOR, - context.mainExecutor, + Dispatchers.IO, callback ) init { - appPredictor?.registerPredictionUpdates(callbackExecutor, appPredictorCallback) + appPredictor?.registerPredictionUpdates(dispatcher.asExecutor(), appPredictorCallback) + lifecycle.coroutineScope + .launch { + appTargetSource.combine(shortcutSource) { appTargets, shortcutData -> + if (appTargets == null || shortcutData == null) { + null + } else { + filterShortcuts( + appTargets, + shortcutData.shortcuts, + shortcutData.isFromAppPredictor, + shortcutData.appPredictorTargets + ) + } + } + .filter { it != null } + .flowOn(dispatcher) + .collect { + callback.accept(it ?: error("can not be null")) + } + } + .invokeOnCompletion { + runCatching { + appPredictor?.unregisterPredictionUpdates(appPredictorCallback) + } + Log.d(TAG, "destroyed, user: $userHandle") + } + reset() } /** - * Unsubscribe from app predictor if one was provided. + * Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */ - @OpenForTesting - @MainThread - open fun destroy() { - isDestroyed = true - appPredictor?.unregisterPredictionUpdates(appPredictorCallback) + fun reset() { + Log.d(TAG, "reset shortcut loader for user $userHandle") + appTargetSource.tryEmit(null) + shortcutSource.tryEmit(null) + lifecycle.coroutineScope.launch(dispatcher) { + loadShortcuts() + } } /** - * Set new resolved targets. This will trigger shortcut loading. - * @param appTargets a collection of application targets a loaded set of shortcuts will be - * grouped against + * Update resolved application targets; as soon as shortcuts are loaded, they will be filtered + * against the targets and the is delivered to the client through the [callback]. */ @OpenForTesting - @MainThread - open fun queryShortcuts(appTargets: Array) { - if (isDestroyed) return - activeRequest.set(Request(appTargets)) - backgroundExecutor.execute { loadShortcuts() } + open fun updateAppTargets(appTargets: Array) { + appTargetSource.tryEmit(appTargets) } @WorkerThread private fun loadShortcuts() { // no need to query direct share for work profile when its locked or disabled if (!shouldQueryDirectShareTargets()) return - Log.d(TAG, "querying direct share targets") + Log.d(TAG, "querying direct share targets for user $userHandle") queryDirectShareTargets(false) } @@ -141,7 +168,7 @@ open class ShortcutLoader @VisibleForTesting constructor( } catch (e: Throwable) { // we might have been destroyed concurrently, nothing left to do if (isDestroyed) return - Log.e(TAG, "Failed to query AppPredictor", e) + Log.e(TAG, "Failed to query AppPredictor for user $userHandle", e) } } // Default to just querying ShortcutManager if AppPredictor not present. @@ -196,6 +223,15 @@ open class ShortcutLoader @VisibleForTesting constructor( isFromAppPredictor: Boolean, appPredictorTargets: List? ) { + shortcutSource.tryEmit(ShortcutData(shortcuts, isFromAppPredictor, appPredictorTargets)) + } + + private fun filterShortcuts( + appTargets: Array, + shortcuts: List, + isFromAppPredictor: Boolean, + appPredictorTargets: List? + ): Result { if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) { throw RuntimeException( "resultList and appTargets must have the same size." @@ -208,7 +244,6 @@ open class ShortcutLoader @VisibleForTesting constructor( // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path // for direct share targets. After ShareSheet is refactored we should use the // ShareShortcutInfos directly. - val appTargets = activeRequest.get().appTargets val resultRecords: MutableList = ArrayList() for (displayResolveInfo in appTargets) { val matchingShortcuts = shortcuts.filter { @@ -225,25 +260,15 @@ open class ShortcutLoader @VisibleForTesting constructor( val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets) resultRecords.add(resultRecord) } - postReport( - Result( - isFromAppPredictor, - appTargets, - resultRecords.toTypedArray(), - directShareAppTargetCache, - directShareShortcutInfoCache - ) + return Result( + isFromAppPredictor, + appTargets, + resultRecords.toTypedArray(), + directShareAppTargetCache, + directShareShortcutInfoCache ) } - private fun postReport(result: Result) = callbackExecutor.execute { report(result) } - - @MainThread - private fun report(result: Result) { - if (isDestroyed) return - callback.accept(result) - } - /** * Returns `false` if `userHandle` is the work profile and it's either * in quiet mode or not running. @@ -256,7 +281,11 @@ open class ShortcutLoader @VisibleForTesting constructor( && userManager.isUserUnlocked(userHandle) && !userManager.isQuietModeEnabled(userHandle) - private class Request(val appTargets: Array) + private class ShortcutData( + val shortcuts: List, + val isFromAppPredictor: Boolean, + val appPredictorTargets: List? + ) /** * Resolved shortcuts with corresponding app targets. @@ -264,7 +293,7 @@ open class ShortcutLoader @VisibleForTesting constructor( class Result( val isFromAppPredictor: Boolean, /** - * Input app targets (see [ShortcutLoader.queryShortcuts] the + * Input app targets (see [ShortcutLoader.updateAppTargets] the * shortcuts were process against. */ val appTargets: Array, @@ -315,7 +344,6 @@ open class ShortcutLoader @VisibleForTesting constructor( companion object { private const val TAG = "ShortcutLoader" - private val NO_REQUEST = Request(arrayOf()) private fun PackageManager.isPackageEnabled(packageName: String): Boolean { if (TextUtils.isEmpty(packageName)) { -- cgit v1.2.3-59-g8ed1b