Don't expose `ChooserTarget` from `TargetInfo` API.
`ChooserTarget` is a deprecated API left over from the (now long-gone)
`ChooserTargetService` system, but internally, `SelectableTargetInfo`
still relies on this type for some basic record-keeping. The
`ChooserTarget`s today come from two sources; they're either passed in
from the caller via `EXTRA_CHOOSER_TARGETS`, or synthesized internally
to shoehorn non-`ChooserTarget` data into a `SelectableTargetInfo`.
Prior to this CL, clients throughout the application might reach
through to the composed-in `ChooserTarget` for some fields stored in
that record type. After this CL, any fields accessed in that way are
instead lifted to the `TargetInfo` API so that clients can access them
directly, without going through a `ChooserTarget`. Thus "consuming"
clients don't need to be aware of the `ChooserTarget`, and from their
perspective it's an internal implementation detail of
`SelectableTargetInfo`.
In a first subsequent CL, these fields can be precomputed from the
`ChooserTarget` during the construction of the `SelectableTargetInfo`
so that we don't retain any instances once we've extracted the
relevant data. In a second subsequent CL, we can expose those fields
as initialization parameters; then instead of synthesizing
intermediate `ChooserTarget`s, we can build our synthetic targets as
`SelectableTargetInfo`s directly. The usage in `EXTRA_CHOOSER_TARGETS`
is regrettably part of our public API that was overlooked in the
`ChooserTargetService` deprecation and won't be going away any time
soon, but if we can adapt them to the `SelectableTargetInfo`
representation immediately when we read them out of the request, we
should be able to cut out any other remaining references to
`ChooserTarget`.
Test: atest IntentResolverUnitTests
Bug: 202167050
Change-Id: I931234b0912952a4ccf5f94a88694ac82b527ae4
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 96f2f9c..76c6fb2 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1723,12 +1723,7 @@
targetInfo.isSelectableTargetInfo() ? getTargetIntentFilter() : null;
String shortcutTitle = targetInfo.isSelectableTargetInfo()
? targetInfo.getDisplayLabel().toString() : null;
- String shortcutIdKey = targetInfo.isSelectableTargetInfo()
- ? targetInfo
- .getChooserTarget()
- .getIntentExtras()
- .getString(Intent.EXTRA_SHORTCUT_ID)
- : null;
+ String shortcutIdKey = targetInfo.getDirectShareShortcutId();
ChooserTargetActionsDialogFragment.show(
getSupportFragmentManager(),
@@ -1816,15 +1811,7 @@
switch (currentListAdapter.getPositionTargetType(which)) {
case ChooserListAdapter.TARGET_SERVICE:
cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
- // Log the package name + target name to answer the question if most users
- // share to mostly the same person or to a bunch of different people.
- ChooserTarget target = targetInfo.getChooserTarget();
- directTargetHashed = HashedStringCache.getInstance().hashString(
- this,
- TAG,
- target.getComponentName().getPackageName()
- + target.getTitle().toString(),
- mMaxHashSaltDays);
+ directTargetHashed = targetInfo.getHashedTargetIdForMetrics(this);
directTargetAlsoRanked = getRankedPosition(targetInfo);
if (mCallerChooserTargets != null) {
@@ -1894,7 +1881,7 @@
private int getRankedPosition(TargetInfo targetInfo) {
String targetPackageName =
- targetInfo.getChooserTarget().getComponentName().getPackageName();
+ targetInfo.getChooserTargetComponentName().getPackageName();
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
int maxRankedResults = Math.min(currentListAdapter.mDisplayList.size(),
@@ -2165,7 +2152,7 @@
ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
if (shortcutInfo != null) {
ComponentName componentName =
- chooserTargetInfo.getChooserTarget().getComponentName();
+ chooserTargetInfo.getChooserTargetComponentName();
targetIds.add(new AppTargetId(
String.format(
"%s/%s/%s",
diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
index 2de901c..8b9bfb3 100644
--- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -16,9 +16,6 @@
package com.android.intentresolver.chooser;
-import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
-
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,24 +39,4 @@
}
return new ArrayList<>(Arrays.asList(getDisplayResolveInfo()));
}
-
- @Override
- public boolean isSimilar(TargetInfo other) {
- if (other == null) return false;
-
- ChooserTarget ct1 = getChooserTarget();
- ChooserTarget ct2 = other.getChooserTarget();
-
- // If either is null, there is not enough info to make an informed decision
- // about equality, so just exit
- if (ct1 == null || ct2 == null) return false;
-
- if (ct1.getComponentName().equals(ct2.getComponentName())
- && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
- && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
- return true;
- }
-
- return false;
- }
}
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index 66e2022..8ec52c8 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -25,7 +25,6 @@
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
-import android.service.chooser.ChooserTarget;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverActivity;
@@ -131,10 +130,6 @@
return -0.1f;
}
- public ChooserTarget getChooserTarget() {
- return null;
- }
-
public boolean isSuspended() {
return false;
}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 3a3c3e6..7e6e49f 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -34,8 +34,10 @@
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
+import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.text.SpannableStringBuilder;
+import android.util.HashedStringCache;
import android.util.Log;
import com.android.intentresolver.ChooserActivity;
@@ -43,6 +45,7 @@
import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
import com.android.intentresolver.SimpleIconFactory;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
import java.util.List;
@@ -54,6 +57,14 @@
public final class SelectableTargetInfo extends ChooserTargetInfo {
private static final String TAG = "SelectableTargetInfo";
+ private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity"; // For legacy reasons.
+ private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
+
+ private final int mMaxHashSaltDays = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
+ DEFAULT_SALT_EXPIRATION_DAYS);
+
private final Context mContext;
@Nullable
private final DisplayResolveInfo mSourceInfo;
@@ -280,6 +291,11 @@
return null;
}
+ @Override
+ public ComponentName getChooserTargetComponentName() {
+ return mChooserTarget.getComponentName();
+ }
+
private Intent getBaseIntentToSend() {
Intent result = getResolvedIntent();
if (result == null) {
@@ -359,11 +375,6 @@
}
@Override
- public ChooserTarget getChooserTarget() {
- return mChooserTarget;
- }
-
- @Override
@Nullable
public ShortcutInfo getDirectShareShortcutInfo() {
return mShortcutInfo;
@@ -395,6 +406,18 @@
return mIsPinned;
}
+ @Override
+ public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ final String plaintext =
+ mChooserTarget.getComponentName().getPackageName()
+ + mChooserTarget.getTitle().toString();
+ return HashedStringCache.getInstance().hashString(
+ context,
+ HASHED_STRING_CACHE_TAG,
+ plaintext,
+ mMaxHashSaltDays);
+ }
+
@Nullable
private static ApplicationInfo getApplicationInfoFromSource(
@Nullable DisplayResolveInfo sourceInfo) {
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index 29c4cfc..46cd53c 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -29,6 +29,8 @@
import android.os.Bundle;
import android.os.UserHandle;
import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.HashedStringCache;
import com.android.intentresolver.ResolverActivity;
@@ -52,13 +54,34 @@
/**
* Get the resolved component name that represents this target. Note that this may not
* be the component that will be directly launched by calling one of the <code>start</code>
- * methods provided; this is the component that will be credited with the launch.
+ * methods provided; this is the component that will be credited with the launch. This may be
+ * null if the target was specified by a caller-provided {@link ChooserTarget} that we failed to
+ * resolve to a component on the system.
*
* @return the resolved ComponentName for this target
*/
+ @Nullable
ComponentName getResolvedComponentName();
/**
+ * If this target was historically built from a (now-deprecated) {@link ChooserTarget} record,
+ * get the {@link ComponentName} that would've been provided by that record.
+ *
+ * TODO: for (historical) {@link ChooserTargetInfo} targets, this differs from the result of
+ * {@link #getResolvedComponentName()} only for caller-provided targets that we fail to resolve;
+ * then this returns the name of the component that was requested, and the other returns null.
+ * At the time of writing, this method is only called in contexts where the client knows that
+ * the target was a historical {@link ChooserTargetInfo}. Thus this method could be removed and
+ * all clients consolidated on the other, if we have some alternate mechanism of tracking this
+ * discrepancy; or if we know that the distinction won't apply in the conditions when we call
+ * this method; or if we determine that tracking the distinction isn't a requirement for us.
+ */
+ @Nullable
+ default ComponentName getChooserTargetComponentName() {
+ return null;
+ }
+
+ /**
* Start the activity referenced by this target.
*
* @param activity calling Activity performing the launch
@@ -172,7 +195,25 @@
* presumably resolved by converting {@code TargetInfo} from an interface to an abstract class.
*/
default boolean isSimilar(TargetInfo other) {
- return Objects.equals(this, other);
+ if (other == null) {
+ return false;
+ }
+
+ // TODO: audit usage and try to reconcile a behavior that doesn't depend on the legacy
+ // subclass type. Note that the `isSimilar()` method was pulled up from the legacy
+ // `ChooserTargetInfo`, so no legacy behavior currently depends on calling `isSimilar()` on
+ // an instance where `isChooserTargetInfo()` would return false (although technically it may
+ // have been possible for the `other` target to be of a different type). Thus we have
+ // flexibility in defining the similarity conditions between pairs of non "chooser" targets.
+ if (isChooserTargetInfo()) {
+ return other.isChooserTargetInfo()
+ && Objects.equals(
+ getChooserTargetComponentName(), other.getChooserTargetComponentName())
+ && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
+ && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo());
+ } else {
+ return !other.isChooserTargetInfo() && Objects.equals(this, other);
+ }
}
/**
@@ -186,24 +227,6 @@
}
/**
- * @return the {@link ChooserTarget} record that contains additional data about this target, if
- * any. This is only non-null for selectable targets (and probably only Direct Share targets?).
- *
- * @deprecated {@link ChooserTarget} (and any other related {@code ChooserTargetService} APIs)
- * got deprecated as part of sunsetting that old system design, but for historical reasons
- * Chooser continues to shoehorn data from other sources into this representation to maintain
- * compatibility with legacy internal APIs. New clients should avoid taking any further
- * dependencies on the {@link ChooserTarget} type; any data they want to query from those
- * records should instead be pulled up to new query methods directly on this class (or on the
- * root {@link TargetInfo}).
- */
- @Deprecated
- @Nullable
- default ChooserTarget getChooserTarget() {
- return null;
- }
-
- /**
* @return the {@link ShortcutManager} data for any shortcut associated with this target.
*/
@Nullable
@@ -212,6 +235,19 @@
}
/**
+ * @return the ID of the shortcut represented by this target, or null if the target didn't come
+ * from a {@link ShortcutManager} shortcut.
+ */
+ @Nullable
+ default String getDirectShareShortcutId() {
+ ShortcutInfo shortcut = getDirectShareShortcutInfo();
+ if (shortcut == null) {
+ return null;
+ }
+ return shortcut.getId();
+ }
+
+ /**
* @return the {@link AppTarget} metadata if this target was sourced from App Prediction
* service, or null otherwise.
*/
@@ -255,11 +291,11 @@
* @return true if this target represents a legacy {@code ChooserTargetInfo}. These objects were
* historically documented as representing "[a] TargetInfo for Direct Share." However, not all
* of these targets are actually *valid* for direct share; e.g. some represent "empty" items
- * (although perhaps only for display in the Direct Share UI?). {@link #getChooserTarget()} will
- * return null for any of these "invalid" items. In even earlier versions, these targets may
- * also have been results from (now-deprecated/unsupported) {@code ChooserTargetService} peers;
- * even though we no longer use these services, we're still shoehorning other target data into
- * the deprecated {@link ChooserTarget} structure for compatibility with some internal APIs.
+ * (although perhaps only for display in the Direct Share UI?). In even earlier versions, these
+ * targets may also have been results from peers in the (now-deprecated/unsupported)
+ * {@code ChooserTargetService} ecosystem; even though we no longer use these services, we're
+ * still shoehorning other target data into the deprecated {@link ChooserTarget} structure for
+ * compatibility with some internal APIs.
* TODO: refactor to clarify the semantics of any target for which this method returns true
* (e.g., are they characterized by their application in the Direct Share UI?), and to remove
* the scaffolding that adapts to and from the {@link ChooserTarget} structure. Eventually, we
@@ -352,6 +388,19 @@
}
/**
+ * @param context caller's context, to provide the {@link SharedPreferences} for use by the
+ * {@link HashedStringCache}.
+ * @return a hashed ID that should be logged along with our target-selection metrics, or null.
+ * The contents of the plaintext are defined for historical reasons, "the package name + target
+ * name to answer the question if most users share to mostly the same person
+ * or to a bunch of different people." Clients should consider this as opaque data for logging
+ * only; they should not rely on any particular semantics about the value.
+ */
+ default HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ return null;
+ }
+
+ /**
* Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called
* before launching the intent as another user.
*/
diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
index f45d592..8581ed0 100644
--- a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
+++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -261,6 +261,8 @@
)
}
+ // TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases
+ // add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`.
private fun assertShortcutsInOrder(
expected: List<ChooserTarget>, actual: List<TargetInfo>, msg: String? = ""
) {
@@ -268,8 +270,13 @@
for (i in expected.indices) {
assertEquals(
"Unexpected item at position $i",
- expected[i],
- actual[i].chooserTarget
+ expected[i].componentName,
+ actual[i].chooserTargetComponentName
+ )
+ assertEquals(
+ "Unexpected item at position $i",
+ expected[i].title,
+ actual[i].displayLabel
)
}
}
diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
index b6d0962..ae9c0f8 100644
--- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
+++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -76,7 +76,9 @@
assertThat(targetInfo.isSelectableTargetInfo()).isTrue()
assertThat(targetInfo.isChooserTargetInfo()).isTrue() // From legacy inheritance model.
assertThat(targetInfo.getDisplayResolveInfo()).isSameInstanceAs(displayInfo)
- assertThat(targetInfo.getChooserTarget()).isSameInstanceAs(chooserTarget)
+ assertThat(targetInfo.getChooserTargetComponentName())
+ .isEqualTo(chooserTarget.getComponentName())
+ assertThat(targetInfo.getDirectShareShortcutId()).isEqualTo(shortcutInfo.getId())
assertThat(targetInfo.getDirectShareShortcutInfo()).isSameInstanceAs(shortcutInfo)
assertThat(targetInfo.getDirectShareAppTarget()).isSameInstanceAs(appTarget)
// TODO: make more meaningful assertions about the behavior of a selectable target.
@@ -147,4 +149,4 @@
// TODO: consider exercising activity-start behavior.
// TODO: consider exercising DisplayResolveInfo base class behavior.
}
-}
\ No newline at end of file
+}