diff options
Diffstat (limited to 'java')
6 files changed, 318 insertions, 131 deletions
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<ShortcutLoader.Result> 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(); @@ -1255,6 +1270,16 @@ public class ChooserActivity extends ResolverActivity implements } @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) { AppPredictor appPredictor = getAppPredictor(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<Result> ) { 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<Array<DisplayResolveInfo>?>( + replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + private val shortcutSource = MutableSharedFlow<ShortcutData?>( + 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<Result> ) : 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<DisplayResolveInfo>) { - if (isDestroyed) return - activeRequest.set(Request(appTargets)) - backgroundExecutor.execute { loadShortcuts() } + open fun updateAppTargets(appTargets: Array<DisplayResolveInfo>) { + 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<AppTarget>? ) { + shortcutSource.tryEmit(ShortcutData(shortcuts, isFromAppPredictor, appPredictorTargets)) + } + + private fun filterShortcuts( + appTargets: Array<DisplayResolveInfo>, + shortcuts: List<ShareShortcutInfo>, + isFromAppPredictor: Boolean, + appPredictorTargets: List<AppTarget>? + ): 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<ShortcutResultInfo> = 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<DisplayResolveInfo>) + private class ShortcutData( + val shortcuts: List<ShareShortcutInfo>, + val isFromAppPredictor: Boolean, + val appPredictorTargets: List<AppTarget>? + ) /** * 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<DisplayResolveInfo>, @@ -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)) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index cbc8d53e..6c659133 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -1342,7 +1342,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1423,7 +1423,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1508,7 +1508,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1583,7 +1583,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -1675,7 +1675,7 @@ public class UnbundledChooserActivityTest { // verify that ShortcutLoader was queried ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); - verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture()); + verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -2174,7 +2174,7 @@ public class UnbundledChooserActivityTest { ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); verify(shortcutLoaders.get(0).first, times(1)) - .queryShortcuts(appTargets.capture()); + .updateAppTargets(appTargets.capture()); // send shortcuts assertThat( @@ -2255,7 +2255,7 @@ public class UnbundledChooserActivityTest { ArgumentCaptor<DisplayResolveInfo[]> appTargets = ArgumentCaptor.forClass(DisplayResolveInfo[].class); verify(shortcutLoaders.get(0).first, times(1)) - .queryShortcuts(appTargets.capture()); + .updateAppTargets(appTargets.capture()); // send shortcuts List<ChooserTarget> serviceTargets = createDirectShareTargets( @@ -2550,12 +2550,12 @@ public class UnbundledChooserActivityTest { .perform(swipeUp()); waitForIdle(); - verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any()); + verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - verify(workProfileShortcutLoader, times(1)).queryShortcuts(any()); + verify(workProfileShortcutLoader, times(1)).updateAppTargets(any()); } @Test diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index e8e2f862..742aac71 100644 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -26,7 +26,9 @@ import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ShortcutManager import android.os.UserHandle import android.os.UserManager +import androidx.lifecycle.Lifecycle import androidx.test.filters.SmallTest +import com.android.intentresolver.TestLifecycleOwner import com.android.intentresolver.any import com.android.intentresolver.argumentCaptor import com.android.intentresolver.capture @@ -36,19 +38,27 @@ import com.android.intentresolver.createShareShortcutInfo import com.android.intentresolver.createShortcutInfo import com.android.intentresolver.mock import com.android.intentresolver.whenever +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Before import org.junit.Test import org.mockito.Mockito.anyInt import org.mockito.Mockito.atLeastOnce import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify -import java.util.concurrent.Executor import java.util.function.Consumer +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest class ShortcutLoaderTest { private val appInfo = ApplicationInfo().apply { @@ -68,7 +78,9 @@ class ShortcutLoaderTest { whenever(createContextAsUser(any(), anyInt())).thenReturn(this) whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager) } - private val executor = ImmediateExecutor() + private val scheduler = TestCoroutineScheduler() + private val dispatcher = UnconfinedTestDispatcher(scheduler) + private val lifecycleOwner = TestLifecycleOwner() private val intentFilter = mock<IntentFilter>() private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>() private val callback = mock<Consumer<ShortcutLoader.Result>>() @@ -79,20 +91,32 @@ class ShortcutLoaderTest { private val appTargets = arrayOf(appTarget) private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + lifecycleOwner.state = Lifecycle.State.CREATED + } + + @After + fun cleanup() { + lifecycleOwner.state = Lifecycle.State.DESTROYED + Dispatchers.resetMain() + } + @Test - fun test_queryShortcuts_result_consistency_with_AppPredictor() { + fun test_loadShortcutsWithAppPredictor_resultIntegrity() { val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) val matchingAppTarget = createAppTarget(matchingShortcutInfo) val shortcuts = listOf( @@ -130,7 +154,7 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_result_consistency_with_ShortcutManager() { + fun test_loadShortcutsWithShortcutManager_resultIntegrity() { val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -142,16 +166,16 @@ class ShortcutLoaderTest { whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, null, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) val resultCaptor = argumentCaptor<ShortcutLoader.Result>() verify(callback, times(1)).accept(capture(resultCaptor)) @@ -175,7 +199,7 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() { + fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() { val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -187,16 +211,16 @@ class ShortcutLoaderTest { whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) verify(appPredictor, times(1)).requestPredictionUpdate() val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() @@ -226,7 +250,7 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_onAppPredictorFailure_fallbackToShortcutManager() { + fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() { val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -240,16 +264,16 @@ class ShortcutLoaderTest { .thenThrow(IllegalStateException("Test exception")) val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, UserHandle.of(0), true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(appTargets) + testSubject.updateAppTargets(appTargets) verify(appPredictor, times(1)).requestPredictionUpdate() @@ -275,32 +299,105 @@ class ShortcutLoaderTest { } @Test - fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() { + fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() { + ShortcutLoader( + context, + lifecycleOwner.lifecycle, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(appPredictor, times(1)).requestPredictionUpdate() + verify(callback, never()).accept(any()) + } + + @Test + fun test_ShortcutLoader_noResultsWithoutAppTargets() { + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock<ShortcutManager> { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = ShortcutLoader( + context, + lifecycleOwner.lifecycle, + null, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(shortcutManager, times(1)).getShareTargets(any()) + verify(callback, never()).accept(any()) + + testSubject.reset() + + verify(shortcutManager, times(2)).getShareTargets(any()) + verify(callback, never()).accept(any()) + + testSubject.updateAppTargets(appTargets) + + verify(shortcutManager, times(2)).getShareTargets(any()) + verify(callback, times(1)).accept(any()) + } + + @Test + fun test_OnLifecycleDestroyed_unsubscribeFromAppPredictor() { + ShortcutLoader( + context, + lifecycleOwner.lifecycle, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + verify(appPredictor, never()).unregisterPredictionUpdates(any()) + + lifecycleOwner.state = Lifecycle.State.DESTROYED + + verify(appPredictor, times(1)).unregisterPredictionUpdates(any()) + } + + @Test + fun test_workProfileNotRunning_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) } @Test - fun test_queryShortcuts_do_not_call_services_for_locked_work_profile() { + fun test_workProfileLocked_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false) } @Test - fun test_queryShortcuts_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() { + fun test_workProfileQuiteModeEnabled_doNotCallServices() { testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true) } @Test - fun test_queryShortcuts_call_services_for_not_running_main_profile() { + fun test_mainProfileNotRunning_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isUserRunning = false) } @Test - fun test_queryShortcuts_call_services_for_locked_main_profile() { + fun test_mainProfileLocked_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isUserUnlocked = false) } @Test - fun test_queryShortcuts_call_services_if_quite_mode_is_enabled_for_main_profile() { + fun test_mainProfileQuiteModeEnabled_callServicesAnyway() { testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) } @@ -320,16 +417,16 @@ class ShortcutLoaderTest { val callback = mock<Consumer<ShortcutLoader.Result>>() val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, userHandle, false, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) verify(appPredictor, never()).requestPredictionUpdate() } @@ -350,23 +447,17 @@ class ShortcutLoaderTest { val callback = mock<Consumer<ShortcutLoader.Result>>() val testSubject = ShortcutLoader( context, + lifecycleOwner.lifecycle, appPredictor, userHandle, true, intentFilter, - executor, - executor, + dispatcher, callback ) - testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock())) + testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock())) verify(appPredictor, times(1)).requestPredictionUpdate() } } - -private class ImmediateExecutor : Executor { - override fun execute(r: Runnable) { - r.run() - } -} |