summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Matt Casey <mrcasey@google.com> 2023-09-12 14:48:54 +0000
committer Matt Casey <mrcasey@google.com> 2023-09-14 14:31:04 +0000
commitfff11532ca7dcfcfd15516b825d249bc0f719dcd (patch)
tree30c3f0294a562ee476fd82de7d504d066583c475
parent76e0aa28953d42180ba4cec5d9c66e178915ed35 (diff)
Workaround for app predictor callback leaking
Test: atest ResolverAppPredictorCallbackTest Bug: 290971946 Change-Id: If402e752b1eff3b19b6222ae3a1b2e2ef619f9f0
-rw-r--r--core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java40
-rw-r--r--core/java/com/android/internal/app/ChooserActivity.java15
-rw-r--r--core/java/com/android/internal/app/ChooserListAdapter.java10
-rw-r--r--core/java/com/android/internal/app/ResolverAppPredictorCallback.java55
-rw-r--r--core/tests/coretests/src/com/android/internal/app/ResolverAppPredictorCallbackTest.java108
5 files changed, 205 insertions, 23 deletions
diff --git a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
index b9f02365bbe7..d2fdc65b2c36 100644
--- a/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
+++ b/core/java/com/android/internal/app/AppPredictionServiceResolverComparator.java
@@ -61,6 +61,8 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
private final ModelBuilder mModelBuilder;
private ResolverComparatorModel mComparatorModel;
+ private ResolverAppPredictorCallback mSortingCallback;
+
// If this is non-null (and this is not destroyed), it means APS is disabled and we should fall
// back to using the ResolverRankerService.
// TODO: responsibility for this fallback behavior can live outside of the AppPrediction client.
@@ -94,6 +96,9 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
// TODO: may not be necessary to build a new model, since we're destroying anyways.
mComparatorModel = mModelBuilder.buildFallbackModel(mResolverRankerService);
}
+ if (mSortingCallback != null) {
+ mSortingCallback.destroy();
+ }
}
@Override
@@ -140,22 +145,27 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
.setClassName(target.name.getClassName())
.build());
}
+
+ if (mSortingCallback != null) {
+ mSortingCallback.destroy();
+ }
+ mSortingCallback = new ResolverAppPredictorCallback(sortedAppTargets -> {
+ if (sortedAppTargets.isEmpty()) {
+ Log.i(TAG, "AppPredictionService disabled. Using resolver.");
+ setupFallbackModel(targets);
+ } else {
+ Log.i(TAG, "AppPredictionService response received");
+ // Skip sending to Handler which takes extra time to dispatch messages.
+ // TODO: the Handler guards some concurrency conditions, so this could
+ // probably result in a race (we're not currently on the Handler thread?).
+ // We'll leave this as-is since we intend to remove the Handler design
+ // shortly, but this is still an unsound shortcut.
+ handleResult(sortedAppTargets);
+ }
+ });
+
mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
- sortedAppTargets -> {
- if (sortedAppTargets.isEmpty()) {
- Log.i(TAG, "AppPredictionService disabled. Using resolver.");
- setupFallbackModel(targets);
- } else {
- Log.i(TAG, "AppPredictionService response received");
- // Skip sending to Handler which takes extra time to dispatch messages.
- // TODO: the Handler guards some concurrency conditions, so this could
- // probably result in a race (we're not currently on the Handler thread?).
- // We'll leave this as-is since we intend to remove the Handler design
- // shortly, but this is still an unsound shortcut.
- handleResult(sortedAppTargets);
- }
- }
- );
+ mSortingCallback.asConsumer());
}
private void setupFallbackModel(List<ResolvedComponentInfo> targets) {
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 2b39bb4eb7a5..6d8512c9d07c 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -24,9 +24,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROS
import static android.content.ContentProvider.getUserIdFromUri;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
-
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-
import static java.lang.annotation.RetentionPolicy.SOURCE;
import android.animation.Animator;
@@ -777,9 +775,9 @@ public class ChooserActivity extends ResolverActivity implements
return appPredictor;
}
- private AppPredictor.Callback createAppPredictorCallback(
+ private ResolverAppPredictorCallback createAppPredictorCallback(
ChooserListAdapter chooserListAdapter) {
- return resultList -> {
+ return new ResolverAppPredictorCallback(resultList -> {
if (isFinishing() || isDestroyed()) {
return;
}
@@ -811,7 +809,7 @@ public class ChooserActivity extends ResolverActivity implements
}
sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList,
chooserListAdapter.getUserHandle());
- };
+ });
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
@@ -2559,10 +2557,13 @@ public class ChooserActivity extends ResolverActivity implements
boolean filterLastUsed, UserHandle userHandle) {
ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
initialIntents, rList, filterLastUsed, userHandle);
- AppPredictor.Callback appPredictorCallback = createAppPredictorCallback(chooserListAdapter);
+ ResolverAppPredictorCallback appPredictorCallbackWrapper =
+ createAppPredictorCallback(chooserListAdapter);
+ AppPredictor.Callback appPredictorCallback = appPredictorCallbackWrapper.asCallback();
AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback);
chooserListAdapter.setAppPredictor(appPredictor);
- chooserListAdapter.setAppPredictorCallback(appPredictorCallback);
+ chooserListAdapter.setAppPredictorCallback(
+ appPredictorCallback, appPredictorCallbackWrapper);
return new ChooserGridAdapter(chooserListAdapter);
}
diff --git a/core/java/com/android/internal/app/ChooserListAdapter.java b/core/java/com/android/internal/app/ChooserListAdapter.java
index 1eecb413adcb..f77e71863125 100644
--- a/core/java/com/android/internal/app/ChooserListAdapter.java
+++ b/core/java/com/android/internal/app/ChooserListAdapter.java
@@ -103,6 +103,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Sorted list of DisplayResolveInfos for the alphabetical app section.
private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
private AppPredictor mAppPredictor;
+ private ResolverAppPredictorCallback mAppPredictorCallbackWrapper;
private AppPredictor.Callback mAppPredictorCallback;
// Represents the UserSpace in which the Initial Intents should be resolved.
@@ -747,8 +748,11 @@ public class ChooserListAdapter extends ResolverListAdapter {
mAppPredictor = appPredictor;
}
- public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
+ public void setAppPredictorCallback(
+ AppPredictor.Callback appPredictorCallback,
+ ResolverAppPredictorCallback appPredictorCallbackWrapper) {
mAppPredictorCallback = appPredictorCallback;
+ mAppPredictorCallbackWrapper = appPredictorCallbackWrapper;
}
public void destroyAppPredictor() {
@@ -757,6 +761,10 @@ public class ChooserListAdapter extends ResolverListAdapter {
getAppPredictor().destroy();
setAppPredictor(null);
}
+
+ if (mAppPredictorCallbackWrapper != null) {
+ mAppPredictorCallbackWrapper.destroy();
+ }
}
/**
diff --git a/core/java/com/android/internal/app/ResolverAppPredictorCallback.java b/core/java/com/android/internal/app/ResolverAppPredictorCallback.java
new file mode 100644
index 000000000000..c35e536275f0
--- /dev/null
+++ b/core/java/com/android/internal/app/ResolverAppPredictorCallback.java
@@ -0,0 +1,55 @@
+/*
+ * 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.internal.app;
+
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+/**
+ * Callback wrapper that works around potential memory leaks in app predictor.
+ *
+ * Nulls the callback itself when destroyed, so at worst you'll leak just this object.
+ */
+public class ResolverAppPredictorCallback {
+ private volatile Consumer<List<AppTarget>> mCallback;
+
+ public ResolverAppPredictorCallback(Consumer<List<AppTarget>> callback) {
+ mCallback = callback;
+ }
+
+ private void notifyCallback(List<AppTarget> list) {
+ Consumer<List<AppTarget>> callback = mCallback;
+ if (callback != null) {
+ callback.accept(Objects.requireNonNullElseGet(list, List::of));
+ }
+ }
+
+ public Consumer<List<AppTarget>> asConsumer() {
+ return this::notifyCallback;
+ }
+
+ public AppPredictor.Callback asCallback() {
+ return this::notifyCallback;
+ }
+
+ public void destroy() {
+ mCallback = null;
+ }
+}
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverAppPredictorCallbackTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverAppPredictorCallbackTest.java
new file mode 100644
index 000000000000..4aca854469f2
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverAppPredictorCallbackTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.internal.app;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetId;
+import android.os.UserHandle;
+
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+@RunWith(AndroidJUnit4.class)
+public class ResolverAppPredictorCallbackTest {
+ private class Callback implements Consumer<List<AppTarget>> {
+ public int count = 0;
+ public List<AppTarget> latest = null;
+ @Override
+ public void accept(List<AppTarget> appTargets) {
+ count++;
+ latest = appTargets;
+ }
+ };
+
+ @Test
+ public void testAsConsumer() {
+ Callback callback = new Callback();
+ ResolverAppPredictorCallback wrapped = new ResolverAppPredictorCallback(callback);
+ assertThat(callback.count).isEqualTo(0);
+
+ List<AppTarget> targets = createAppTargetList();
+ wrapped.asConsumer().accept(targets);
+
+ assertThat(callback.count).isEqualTo(1);
+ assertThat(callback.latest).isEqualTo(targets);
+
+ wrapped.destroy();
+
+ // Shouldn't do anything:
+ wrapped.asConsumer().accept(targets);
+
+ assertThat(callback.count).isEqualTo(1);
+ }
+
+ @Test
+ public void testAsCallback() {
+ Callback callback = new Callback();
+ ResolverAppPredictorCallback wrapped = new ResolverAppPredictorCallback(callback);
+ assertThat(callback.count).isEqualTo(0);
+
+ List<AppTarget> targets = createAppTargetList();
+ wrapped.asCallback().onTargetsAvailable(targets);
+
+ assertThat(callback.count).isEqualTo(1);
+ assertThat(callback.latest).isEqualTo(targets);
+
+ wrapped.destroy();
+
+ // Shouldn't do anything:
+ wrapped.asConsumer().accept(targets);
+
+ assertThat(callback.count).isEqualTo(1);
+ }
+
+ @Test
+ public void testAsConsumer_null() {
+ Callback callback = new Callback();
+ ResolverAppPredictorCallback wrapped = new ResolverAppPredictorCallback(callback);
+ assertThat(callback.count).isEqualTo(0);
+
+ wrapped.asConsumer().accept(null);
+
+ assertThat(callback.count).isEqualTo(1);
+ assertThat(callback.latest).isEmpty();
+
+ wrapped.destroy();
+
+ // Shouldn't do anything:
+ wrapped.asConsumer().accept(null);
+
+ assertThat(callback.count).isEqualTo(1);
+ }
+
+ private List<AppTarget> createAppTargetList() {
+ AppTarget.Builder builder = new AppTarget.Builder(
+ new AppTargetId("ID"), "package", UserHandle.CURRENT);
+ return List.of(builder.build());
+ }
+}