summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java54
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java18
-rw-r--r--java/src/com/android/intentresolver/measurements/Tracer.kt46
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt150
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java18
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt163
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()
- }
-}