From c9b17c7ea3596aa2698149c592e623a3a492b70a Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 9 May 2023 16:21:03 -0700 Subject: Add traces for shortcuts, app targets and icons Add traces and extra logging for shortcuts fetching, app targets resolution and shortcut and app icons loading. Bug: 280653893 Test: main functinality smoke testing; test traces collected. Change-Id: I68fd624fb0e747947bf9f0d704adec7697a190d0 --- .../android/intentresolver/ChooserActivity.java | 9 +- .../android/intentresolver/ChooserListAdapter.java | 3 + .../ChooserMultiProfilePagerAdapter.java | 17 ++ .../intentresolver/ResolverListAdapter.java | 3 + .../android/intentresolver/measurements/Tracer.kt | 110 ++++++++++- .../intentresolver/shortcuts/ShortcutLoader.kt | 215 +++++++++++---------- 6 files changed, 252 insertions(+), 105 deletions(-) (limited to 'java') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 84e14d72..ca29b6f0 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1550,6 +1550,10 @@ public class ChooserActivity extends ResolverActivity implements } if (rebuildComplete) { + long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle()); + if (duration >= 0) { + Log.d(TAG, "app target loading time " + duration + " ms"); + } addCallerChooserTargets(); getChooserActivityLogger().logSharesheetAppLoadComplete(); maybeQueryAdditionalPostProcessingTargets(chooserListAdapter); @@ -1591,7 +1595,10 @@ public class ChooserActivity extends ResolverActivity implements } if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) { - Tracer.INSTANCE.endLaunchToShortcutTrace(); + long duration = Tracer.INSTANCE.endLaunchToShortcutTrace(); + if (duration >= 0) { + Log.d(TAG, "stat to first shortcut time: " + duration + " ms"); + } } logDirectShareTargetReceived(userHandle); sendVoiceChoicesIfNeeded(); diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index ebbf5515..c20af20c 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -676,6 +676,7 @@ public class ChooserListAdapter extends ResolverListAdapter { @Override protected Drawable doInBackground(Void... voids) { Drawable drawable; + Trace.beginSection("shortcut-icon"); try { drawable = getChooserTargetIconDrawable( mContext, @@ -688,6 +689,8 @@ public class ChooserListAdapter extends ResolverListAdapter { + mTargetInfo.getChooserTargetComponentName(), e); drawable = loadIconPlaceholder(); + } finally { + Trace.endSection(); } return drawable; } diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index 9c096fd2..da7dc998 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager.widget.PagerAdapter; import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.measurements.Tracer; import com.android.internal.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; @@ -130,6 +131,22 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda return rootView; } + @Override + boolean rebuildActiveTab(boolean doPostProcessing) { + if (doPostProcessing) { + Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle()); + } + return super.rebuildActiveTab(doPostProcessing); + } + + @Override + boolean rebuildInactiveTab(boolean doPostProcessing) { + if (getItemCount() != 1 && doPostProcessing) { + Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle()); + } + return super.rebuildInactiveTab(doPostProcessing); + } + private static class BottomPaddingOverrideSupplier implements Supplier> { private final Context mContext; private int mBottomOffset; diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index fb7641b3..a5fdd320 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -1034,12 +1034,15 @@ public class ResolverListAdapter extends BaseAdapter { @Override protected Drawable doInBackground(Void... params) { + Trace.beginSection("app-icon"); try { return loadIconForResolveInfo(mResolveInfo); } catch (Exception e) { ComponentName componentName = mDisplayResolveInfo.getResolvedComponentName(); Log.e(TAG, "Failed to load app icon for " + componentName, e); return loadIconPlaceholder(); + } finally { + Trace.endSection(); } } diff --git a/java/src/com/android/intentresolver/measurements/Tracer.kt b/java/src/com/android/intentresolver/measurements/Tracer.kt index f7e01879..5f69932a 100644 --- a/java/src/com/android/intentresolver/measurements/Tracer.kt +++ b/java/src/com/android/intentresolver/measurements/Tracer.kt @@ -18,14 +18,20 @@ package com.android.intentresolver.measurements import android.os.SystemClock import android.os.Trace -import android.util.Log +import android.os.UserHandle +import android.util.SparseArray +import androidx.annotation.GuardedBy +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong -private const val TAG = "Tracer" private const val SECTION_LAUNCH_TO_SHORTCUT = "launch-to-shortcut" +private const val SECTION_APP_PREDICTOR_PREFIX = "app-predictor-" +private const val SECTION_APP_TARGET_PREFIX = "app-target-" object Tracer { private val launchToFirstShortcut = AtomicLong(-1L) + private val nextId = AtomicInteger(0) + @GuardedBy("self") private val profileRecords = SparseArray() fun markLaunched() { if (launchToFirstShortcut.compareAndSet(-1, elapsedTimeNow())) { @@ -33,18 +39,112 @@ object Tracer { } } - fun endLaunchToShortcutTrace() { + fun endLaunchToShortcutTrace(): Long { val time = elapsedTimeNow() val startTime = launchToFirstShortcut.get() - if (startTime >= 0 && launchToFirstShortcut.compareAndSet(startTime, -1L)) { + return 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") + time - startTime + } else { + -1L } } + /** + * Begin shortcuts request tracing. The logic is based on an assumption that each request for + * shortcuts update is followed by at least one response. Note, that it is not always measure + * the request duration correctly as in the case of a two overlapping requests when the second + * requests starts and ends while the first is running, the end of the second request will be + * attributed to the first. This is tolerable as this still represents the visible to the user + * app's behavior and expected to be quite rare. + */ + fun beginAppPredictorQueryTrace(userHandle: UserHandle) { + val queue = getUserShortcutRequestQueue(userHandle, createIfMissing = true) ?: return + val startTime = elapsedTimeNow() + val id = nextId.getAndIncrement() + val sectionName = userHandle.toAppPredictorSectionName() + synchronized(queue) { + Trace.beginAsyncSection(sectionName, id) + queue.addFirst(longArrayOf(startTime, id.toLong())) + } + } + + /** + * End shortcut request tracing, see [beginAppPredictorQueryTrace]. + * + * @return request duration is milliseconds. + */ + fun endAppPredictorQueryTrace(userHandle: UserHandle): Long { + val queue = getUserShortcutRequestQueue(userHandle, createIfMissing = false) ?: return -1L + val endTime = elapsedTimeNow() + val sectionName = userHandle.toAppPredictorSectionName() + return synchronized(queue) { queue.removeLastOrNull() } + ?.let { record -> + Trace.endAsyncSection(sectionName, record[1].toInt()) + endTime - record[0] + } + ?: -1L + } + + /** + * Trace app target loading section per profile. If there's already an active section, it will + * be ended an a new section started. + */ + fun beginAppTargetLoadingSection(userHandle: UserHandle) { + val profile = getProfileRecord(userHandle, createIfMissing = true) ?: return + val sectionName = userHandle.toAppTargetSectionName() + val time = elapsedTimeNow() + synchronized(profile) { + if (profile.appTargetLoading >= 0) { + Trace.endAsyncSection(sectionName, 0) + } + profile.appTargetLoading = time + Trace.beginAsyncSection(sectionName, 0) + } + } + + fun endAppTargetLoadingSection(userHandle: UserHandle): Long { + val profile = getProfileRecord(userHandle, createIfMissing = false) ?: return -1L + val time = elapsedTimeNow() + val sectionName = userHandle.toAppTargetSectionName() + return synchronized(profile) { + if (profile.appTargetLoading >= 0) { + Trace.endAsyncSection(sectionName, 0) + (time - profile.appTargetLoading).also { profile.appTargetLoading = -1L } + } else { + -1L + } + } + } + + private fun getUserShortcutRequestQueue( + userHandle: UserHandle, + createIfMissing: Boolean + ): ArrayDeque? = getProfileRecord(userHandle, createIfMissing)?.appPredictorRequests + + private fun getProfileRecord(userHandle: UserHandle, createIfMissing: Boolean): ProfileRecord? = + synchronized(profileRecords) { + val idx = profileRecords.indexOfKey(userHandle.identifier) + when { + idx >= 0 -> profileRecords.valueAt(idx) + createIfMissing -> + ProfileRecord().also { profileRecords.put(userHandle.identifier, it) } + else -> null + } + } + private fun elapsedTimeNow() = SystemClock.elapsedRealtime() } +private class ProfileRecord { + val appPredictorRequests = ArrayDeque() + @GuardedBy("this") var appTargetLoading = -1L +} + +private fun UserHandle.toAppPredictorSectionName() = SECTION_APP_PREDICTOR_PREFIX + identifier + +private fun UserHandle.toAppTargetSectionName() = SECTION_APP_TARGET_PREFIX + identifier + inline fun runTracing(name: String, block: () -> R): R { Trace.beginSection(name) try { diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt index ee6893d0..3ffbe039 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -38,6 +38,10 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope import com.android.intentresolver.chooser.DisplayResolveInfo +import com.android.intentresolver.measurements.Tracer +import com.android.intentresolver.measurements.runTracing +import java.util.concurrent.Executor +import java.util.function.Consumer import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor @@ -47,20 +51,19 @@ 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.function.Consumer /** * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager. * - * * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut - * 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. + * 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( +open class ShortcutLoader +@VisibleForTesting +constructor( private val context: Context, private val lifecycle: Lifecycle, private val appPredictor: AppPredictorProxy?, @@ -73,13 +76,15 @@ open class ShortcutLoader @VisibleForTesting constructor( private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter() private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) } - 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) + 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( @@ -93,7 +98,8 @@ open class ShortcutLoader @VisibleForTesting constructor( context, lifecycle, appPredictor?.let { AppPredictorProxy(it) }, - userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()), + userHandle, + userHandle == UserHandle.of(ActivityManager.getCurrentUser()), targetIntentFilter, Dispatchers.IO, callback @@ -103,43 +109,38 @@ open class ShortcutLoader @VisibleForTesting constructor( 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 - ) + appTargetSource + .combine(shortcutSource) { appTargets, shortcutData -> + if (appTargets == null || shortcutData == null) { + null + } else { + runTracing("filter-shortcuts-${userHandle.identifier}") { + filterShortcuts( + appTargets, + shortcutData.shortcuts, + shortcutData.isFromAppPredictor, + shortcutData.appPredictorTargets + ) + } + } } - } - .filter { it != null } - .flowOn(dispatcher) - .collect { - callback.accept(it ?: error("can not be null")) - } + .filter { it != null } + .flowOn(dispatcher) + .collect { callback.accept(it ?: error("can not be null")) } } .invokeOnCompletion { - runCatching { - appPredictor?.unregisterPredictionUpdates(appPredictorCallback) - } + runCatching { appPredictor?.unregisterPredictionUpdates(appPredictorCallback) } Log.d(TAG, "destroyed, user: $userHandle") } reset() } - /** - * Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. - */ + /** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */ fun reset() { Log.d(TAG, "reset shortcut loader for user $userHandle") appTargetSource.tryEmit(null) shortcutSource.tryEmit(null) - lifecycle.coroutineScope.launch(dispatcher) { - loadShortcuts() - } + lifecycle.coroutineScope.launch(dispatcher) { loadShortcuts() } } /** @@ -154,7 +155,10 @@ open class ShortcutLoader @VisibleForTesting constructor( @WorkerThread private fun loadShortcuts() { // no need to query direct share for work profile when its locked or disabled - if (!shouldQueryDirectShareTargets()) return + if (!shouldQueryDirectShareTargets()) { + Log.d(TAG, "skip shortcuts loading for user $userHandle") + return + } Log.d(TAG, "querying direct share targets for user $userHandle") queryDirectShareTargets(false) } @@ -163,33 +167,49 @@ open class ShortcutLoader @VisibleForTesting constructor( private fun queryDirectShareTargets(skipAppPredictionService: Boolean) { if (!skipAppPredictionService && appPredictor != null) { try { + Log.d(TAG, "query AppPredictor for user $userHandle") + Tracer.beginAppPredictorQueryTrace(userHandle) appPredictor.requestPredictionUpdate() return } catch (e: Throwable) { + endAppPredictorQueryTrace(userHandle) // we might have been destroyed concurrently, nothing left to do - if (isDestroyed) return + if (isDestroyed) { + return + } Log.e(TAG, "Failed to query AppPredictor for user $userHandle", e) } } // Default to just querying ShortcutManager if AppPredictor not present. - if (targetIntentFilter == null) return - val shortcuts = queryShortcutManager(targetIntentFilter) + if (targetIntentFilter == null) { + Log.d(TAG, "skip querying ShortcutManager for $userHandle") + return + } + Log.d(TAG, "query ShortcutManager for user $userHandle") + val shortcuts = + runTracing("shortcut-mngr-${userHandle.identifier}") { + queryShortcutManager(targetIntentFilter) + } + Log.d(TAG, "receive shortcuts from ShortcutManager for user $userHandle") sendShareShortcutInfoList(shortcuts, false, null) } @WorkerThread private fun queryShortcutManager(targetIntentFilter: IntentFilter): List { val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */) - val sm = selectedProfileContext - .getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager? + val sm = + selectedProfileContext.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager? val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager - return sm?.getShareTargets(targetIntentFilter) - ?.filter { pm.isPackageEnabled(it.targetComponent.packageName) } + return sm?.getShareTargets(targetIntentFilter)?.filter { + pm.isPackageEnabled(it.targetComponent.packageName) + } ?: emptyList() } @WorkerThread private fun onAppPredictorCallback(appPredictorTargets: List) { + endAppPredictorQueryTrace(userHandle) + Log.d(TAG, "receive app targets from AppPredictor") if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) { // APS may be disabled, so try querying targets ourselves. queryDirectShareTargets(true) @@ -202,9 +222,7 @@ open class ShortcutLoader @VisibleForTesting constructor( @WorkerThread private fun List.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair = - fold( - ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size)) - ) { acc, appTarget -> + fold(ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size))) { acc, appTarget -> val shortcutInfo = appTarget.shortcutInfo val packageName = appTarget.packageName val className = appTarget.className @@ -234,9 +252,11 @@ open class ShortcutLoader @VisibleForTesting constructor( ): Result { if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) { throw RuntimeException( - "resultList and appTargets must have the same size." - + " resultList.size()=" + shortcuts.size - + " appTargets.size()=" + appPredictorTargets.size + "resultList and appTargets must have the same size." + + " resultList.size()=" + + shortcuts.size + + " appTargets.size()=" + + appPredictorTargets.size ) } val directShareAppTargetCache = HashMap() @@ -246,17 +266,17 @@ open class ShortcutLoader @VisibleForTesting constructor( // ShareShortcutInfos directly. val resultRecords: MutableList = ArrayList() for (displayResolveInfo in appTargets) { - val matchingShortcuts = shortcuts.filter { - it.targetComponent == displayResolveInfo.resolvedComponentName - } + val matchingShortcuts = + shortcuts.filter { it.targetComponent == displayResolveInfo.resolvedComponentName } if (matchingShortcuts.isEmpty()) continue - val chooserTargets = shortcutToChooserTargetConverter.convertToChooserTarget( - matchingShortcuts, - shortcuts, - appPredictorTargets, - directShareAppTargetCache, - directShareShortcutInfoCache - ) + val chooserTargets = + shortcutToChooserTargetConverter.convertToChooserTarget( + matchingShortcuts, + shortcuts, + appPredictorTargets, + directShareAppTargetCache, + directShareShortcutInfoCache + ) val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets) resultRecords.add(resultRecord) } @@ -270,16 +290,17 @@ open class ShortcutLoader @VisibleForTesting constructor( } /** - * Returns `false` if `userHandle` is the work profile and it's either - * in quiet mode or not running. + * Returns `false` if `userHandle` is the work profile and it's either in quiet mode or not + * running. */ private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive @get:VisibleForTesting protected val isProfileActive: Boolean - get() = userManager.isUserRunning(userHandle) - && userManager.isUserUnlocked(userHandle) - && !userManager.isQuietModeEnabled(userHandle) + get() = + userManager.isUserRunning(userHandle) && + userManager.isUserUnlocked(userHandle) && + !userManager.isQuietModeEnabled(userHandle) private class ShortcutData( val shortcuts: List, @@ -287,27 +308,21 @@ open class ShortcutLoader @VisibleForTesting constructor( val appPredictorTargets: List? ) - /** - * Resolved shortcuts with corresponding app targets. - */ + /** Resolved shortcuts with corresponding app targets. */ class Result( val isFromAppPredictor: Boolean, /** - * Input app targets (see [ShortcutLoader.updateAppTargets] the - * shortcuts were process against. + * Input app targets (see [ShortcutLoader.updateAppTargets] the shortcuts were process + * against. */ val appTargets: Array, - /** - * Shortcuts grouped by app target. - */ + /** Shortcuts grouped by app target. */ val shortcutsByApp: Array, val directShareAppTargetCache: Map, val directShareShortcutInfoCache: Map ) - /** - * Shortcuts grouped by app. - */ + /** Shortcuts grouped by app. */ class ShortcutResultInfo( val appTarget: DisplayResolveInfo, val shortcuts: List @@ -318,27 +333,20 @@ open class ShortcutLoader @VisibleForTesting constructor( val appTargets: List? ) - /** - * A wrapper around AppPredictor to facilitate unit-testing. - */ + /** A wrapper around AppPredictor to facilitate unit-testing. */ @VisibleForTesting open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) { - /** - * [AppPredictor.registerPredictionUpdates] - */ + /** [AppPredictor.registerPredictionUpdates] */ open fun registerPredictionUpdates( - callbackExecutor: Executor, callback: AppPredictor.Callback + callbackExecutor: Executor, + callback: AppPredictor.Callback ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback) - /** - * [AppPredictor.unregisterPredictionUpdates] - */ + /** [AppPredictor.unregisterPredictionUpdates] */ open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) = mAppPredictor.unregisterPredictionUpdates(callback) - /** - * [AppPredictor.requestPredictionUpdate] - */ + /** [AppPredictor.requestPredictionUpdate] */ open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate() } @@ -350,12 +358,21 @@ open class ShortcutLoader @VisibleForTesting constructor( return false } return runCatching { - val appInfo = getApplicationInfo( - packageName, - PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong()) - ) - appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0 - }.getOrDefault(false) + val appInfo = + getApplicationInfo( + packageName, + PackageManager.ApplicationInfoFlags.of( + PackageManager.GET_META_DATA.toLong() + ) + ) + appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0 + } + .getOrDefault(false) + } + + private fun endAppPredictorQueryTrace(userHandle: UserHandle) { + val duration = Tracer.endAppPredictorQueryTrace(userHandle) + Log.d(TAG, "AppPredictor query duration for user $userHandle: $duration ms") } } } -- cgit v1.2.3-59-g8ed1b