summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
Diffstat (limited to 'java/src')
-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
4 files changed, 182 insertions, 86 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)) {