From 6333997c2c04f2e71c8fefad56a651e88bd25922 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Sun, 5 Feb 2023 04:03:56 +0000 Subject: [intentresolver] Reselection -> modify share Doesn't change the UI or logging in this change, just API and code naming. Bug: 267870268 Test: atest UnbundledChooserActivityTest Change-Id: I2b8a68ed1e3fe1e6d4bdb1a89f155afdf377159a --- java/src/com/android/intentresolver/ChooserActivity.java | 4 ++-- .../android/intentresolver/ChooserContentPreviewUi.java | 16 ++++++++-------- .../android/intentresolver/ChooserRequestParameters.java | 16 ++++++++-------- 3 files changed, 18 insertions(+), 18 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 3a7d4e68..c11b8060 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -763,12 +763,12 @@ public class ChooserActivity extends ResolverActivity implements @Nullable @Override - public Runnable getReselectionAction() { + public Runnable getModifyShareAction() { if (!mFeatureFlagRepository .isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) { return null; } - PendingIntent reselectionAction = mChooserRequest.getReselectionAction(); + PendingIntent reselectionAction = mChooserRequest.getModifyShareAction(); return reselectionAction == null ? null : createReselectionRunnable(reselectionAction); diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java index 91abd9d0..a294e1a1 100644 --- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java @@ -98,10 +98,10 @@ public final class ChooserContentPreviewUi { List createCustomActions(); /** - * Provides a re-selection action, if any. + * Provides a share modification action, if any. */ @Nullable - Runnable getReselectionAction(); + Runnable getModifyShareAction(); /** *

@@ -254,13 +254,13 @@ public final class ChooserContentPreviewUi { default: Log.e(TAG, "Unexpected content preview type: " + previewType); } - Runnable reselectionAction = actionFactory.getReselectionAction(); - if (reselectionAction != null && layout != null + Runnable modifyShareAction = actionFactory.getModifyShareAction(); + if (modifyShareAction != null && layout != null && mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) { - View reselectionView = layout.findViewById(R.id.reselection_action); - if (reselectionView != null) { - reselectionView.setVisibility(View.VISIBLE); - reselectionView.setOnClickListener(view -> reselectionAction.run()); + View modifyShareView = layout.findViewById(R.id.reselection_action); + if (modifyShareView != null) { + modifyShareView.setVisibility(View.VISIBLE); + modifyShareView.setOnClickListener(view -> modifyShareAction.run()); } } diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 0d004b0d..2b67b273 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -76,7 +76,7 @@ public class ChooserRequestParameters { private final ImmutableList mFilteredComponentNames; private final ImmutableList mCallerChooserTargets; private final ImmutableList mChooserActions; - private final PendingIntent mReselectionAction; + private final PendingIntent mModifyShareAction; private final boolean mRetainInOnStop; @Nullable @@ -142,8 +142,8 @@ public class ChooserRequestParameters { mChooserActions = featureFlags.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS) ? getChooserActions(clientIntent) : ImmutableList.of(); - mReselectionAction = featureFlags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) - ? getReselectionActionExtra(clientIntent) + mModifyShareAction = featureFlags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) + ? getModifyShareAction(clientIntent) : null; } @@ -191,8 +191,8 @@ public class ChooserRequestParameters { } @Nullable - public PendingIntent getReselectionAction() { - return mReselectionAction; + public PendingIntent getModifyShareAction() { + return mModifyShareAction; } /** @@ -335,15 +335,15 @@ public class ChooserRequestParameters { } @Nullable - private static PendingIntent getReselectionActionExtra(Intent intent) { + private static PendingIntent getModifyShareAction(Intent intent) { try { return intent.getParcelableExtra( - Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION, + Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, PendingIntent.class); } catch (Throwable t) { Log.w( TAG, - "Unable to retrieve Intent.EXTRA_CHOOSER_PAYLOAD_RESELECTION_ACTION argument", + "Unable to retrieve Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION argument", t); return null; } -- cgit v1.2.3-59-g8ed1b From 5b4432dbb7972e99a8ae30fa7421b805116d691d Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Mon, 6 Feb 2023 21:49:25 +0000 Subject: Release sharesheet API and preview features. Update flag and test code to allow for released flags. Test: atest CtsSharesheetDeviceTest Test: atest UnbundledChooserActivityTest Bug: 266983432 Bug: 266982749 Bug: 266983474 Bug: 267355521 Change-Id: I1c45840bd454264f553d6d25c4029f3cc8f3f073 --- java/src/com/android/intentresolver/flags/Flags.kt | 14 +++++++++----- .../android/intentresolver/TestFeatureFlagRepository.kt | 6 ++++-- 2 files changed, 13 insertions(+), 7 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 9c206265..917983ed 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.flags +import com.android.systemui.flags.ReleasedFlag import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to @@ -23,24 +24,27 @@ import com.android.systemui.flags.UnreleasedFlag object Flags { // TODO(b/266983432) Tracking Bug @JvmField - val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(1501, "sharesheet_custom_actions", teamfood = true) + val SHARESHEET_CUSTOM_ACTIONS = releasedFlag(1501, "sharesheet_custom_actions") // TODO(b/266982749) Tracking Bug @JvmField - val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(1502, "sharesheet_reselection_action", teamfood = true) + val SHARESHEET_RESELECTION_ACTION = releasedFlag(1502, "sharesheet_reselection_action") // TODO(b/266983474) Tracking Bug @JvmField - val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag( - id = 1503, name = "sharesheet_image_text_preview", teamfood = true + val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = releasedFlag( + id = 1503, name = "sharesheet_image_text_preview" ) // TODO(b/267355521) Tracking Bug @JvmField - val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = unreleasedFlag( + val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag( 1504, "sharesheet_scrollable_image_preview" ) + private fun releasedFlag(id: Int, name: String) = + ReleasedFlag(id, name, "systemui") + private fun unreleasedFlag(id: Int, name: String, teamfood: Boolean = false) = UnreleasedFlag(id, name, "systemui", teamfood) } diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt index abc24efb..b8f6a5c1 100644 --- a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt +++ b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt @@ -17,14 +17,16 @@ package com.android.intentresolver import com.android.intentresolver.flags.FeatureFlagRepository +import com.android.systemui.flags.BooleanFlag import com.android.systemui.flags.ReleasedFlag import com.android.systemui.flags.UnreleasedFlag internal class TestFeatureFlagRepository( - private val overrides: Map + private val overrides: Map ) : FeatureFlagRepository { override fun isEnabled(flag: UnreleasedFlag): Boolean = overrides.getOrDefault(flag, flag.default) - override fun isEnabled(flag: ReleasedFlag): Boolean = flag.default + override fun isEnabled(flag: ReleasedFlag): Boolean = + overrides.getOrDefault(flag, flag.default) } -- cgit v1.2.3-59-g8ed1b From 880417ed82485d63c87737d38a270a3367d27594 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 6 Feb 2023 21:31:44 -0800 Subject: Run Chooser integration tests for the new features Add a new test parameter, feature flag set, and run Chooser tests for two set values: all flags are off and all flags are on. This ensures that our integration tests cover the new features. A new test rule is added to ignore tests that were designed for a specific flag values when running in a set that does not have those values set. Bug: 258838272 Test: the modified test itself Change-Id: If646ef123a383e801fda55d601e10b186c6c5c1f --- java/src/com/android/intentresolver/flags/Flags.kt | 13 ++- .../com/android/intentresolver/FeatureFlagRule.kt | 56 ++++++++++ .../android/intentresolver/RequireFeatureFlags.kt | 23 +++++ .../intentresolver/TestFeatureFlagRepository.kt | 7 +- .../UnbundledChooserActivityTest.java | 114 +++++++++++++-------- 5 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 java/tests/src/com/android/intentresolver/FeatureFlagRule.kt create mode 100644 java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 917983ed..40f32bf3 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -22,24 +22,29 @@ import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to // make the flags available in the flag flipper app (see go/sysui-flags). object Flags { + const val SHARESHEET_CUSTOM_ACTIONS_NAME = "sharesheet_custom_actions" + const val SHARESHEET_RESELECTION_ACTION_NAME = "sharesheet_reselection_action" + const val SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME = "sharesheet_image_text_preview" + const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview" + // TODO(b/266983432) Tracking Bug @JvmField - val SHARESHEET_CUSTOM_ACTIONS = releasedFlag(1501, "sharesheet_custom_actions") + val SHARESHEET_CUSTOM_ACTIONS = releasedFlag(1501, SHARESHEET_CUSTOM_ACTIONS_NAME) // TODO(b/266982749) Tracking Bug @JvmField - val SHARESHEET_RESELECTION_ACTION = releasedFlag(1502, "sharesheet_reselection_action") + val SHARESHEET_RESELECTION_ACTION = releasedFlag(1502, SHARESHEET_RESELECTION_ACTION_NAME) // TODO(b/266983474) Tracking Bug @JvmField val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = releasedFlag( - id = 1503, name = "sharesheet_image_text_preview" + id = 1503, name = SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME ) // TODO(b/267355521) Tracking Bug @JvmField val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag( - 1504, "sharesheet_scrollable_image_preview" + 1504, SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME ) private fun releasedFlag(id: Int, name: String) = diff --git a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt new file mode 100644 index 00000000..3fa01bcc --- /dev/null +++ b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt @@ -0,0 +1,56 @@ +/* + * 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 + +import com.android.systemui.flags.BooleanFlag +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +/** + * Ignores tests annotated with [RequireFeatureFlags] which flag requirements does not + * meet in the active flag set. + * @param flags active flag set + */ +internal class FeatureFlagRule(flags: Map) : TestRule { + private val flags = flags.entries.fold(HashMap()) { map, (key, value) -> + map.apply { + put(key.name, value) + } + } + private val skippingStatement = object : Statement() { + override fun evaluate() = Unit + } + + override fun apply(base: Statement, description: Description): Statement { + val annotation = description.annotations.firstOrNull { + it is RequireFeatureFlags + } as? RequireFeatureFlags + ?: return base + + if (annotation.flags.size != annotation.values.size) { + error("${description.className}#${description.methodName}: inconsistent number of" + + " flags and values in $annotation") + } + for (i in annotation.flags.indices) { + val flag = annotation.flags[i] + val value = annotation.values[i] + if (flags.getOrDefault(flag, !value) != value) return skippingStatement + } + return base + } +} diff --git a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt new file mode 100644 index 00000000..1ddf7462 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt @@ -0,0 +1,23 @@ +/* + * 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 + +/** + * Specifies expected feature flag values for a test. + */ +@Target(AnnotationTarget.FUNCTION) +annotation class RequireFeatureFlags(val flags: Array, val values: BooleanArray) diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt index b8f6a5c1..5a159d24 100644 --- a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt +++ b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt @@ -24,9 +24,8 @@ import com.android.systemui.flags.UnreleasedFlag internal class TestFeatureFlagRepository( private val overrides: Map ) : FeatureFlagRepository { - override fun isEnabled(flag: UnreleasedFlag): Boolean = - overrides.getOrDefault(flag, flag.default) + override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag) + override fun isEnabled(flag: ReleasedFlag): Boolean = getValue(flag) - override fun isEnabled(flag: ReleasedFlag): Boolean = - overrides.getOrDefault(flag, flag.default) + private fun getValue(flag: BooleanFlag) = overrides.getOrDefault(flag, flag.default) } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 17fd5bd9..aaf7a252 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -107,6 +107,7 @@ import com.android.intentresolver.flags.Flags; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.systemui.flags.BooleanFlag; import org.hamcrest.Description; import org.hamcrest.Matcher; @@ -115,6 +116,8 @@ import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.mockito.ArgumentCaptor; @@ -123,7 +126,6 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -162,14 +164,40 @@ public class UnbundledChooserActivityTest { return mock; }; + private static final List ALL_FLAGS = + Arrays.asList( + Flags.SHARESHEET_CUSTOM_ACTIONS, + Flags.SHARESHEET_RESELECTION_ACTION, + Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, + Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); + + private static final Map ALL_FLAGS_OFF = + createAllFlagsOverride(false); + private static final Map ALL_FLAGS_ON = + createAllFlagsOverride(true); + @Parameterized.Parameters public static Collection packageManagers() { return Arrays.asList(new Object[][] { - {0, "Default PackageManager", DEFAULT_PM}, - {1, "No App Prediction Service", NO_APP_PREDICTION_SERVICE_PM} + // Default PackageManager and all flags off + { DEFAULT_PM, ALL_FLAGS_OFF}, + // Default PackageManager and all flags on + { DEFAULT_PM, ALL_FLAGS_ON}, + // No App Prediction Service and all flags off + { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, + // No App Prediction Service and all flags on + { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_ON } }); } + private static Map createAllFlagsOverride(boolean value) { + HashMap overrides = new HashMap<>(ALL_FLAGS.size()); + for (BooleanFlag flag : ALL_FLAGS) { + overrides.put(flag, value); + } + return overrides; + } + /* -------- * Subclasses can override the following methods to customize test behavior. * -------- @@ -189,6 +217,8 @@ public class UnbundledChooserActivityTest { .adoptShellPermissionIdentity(); cleanOverrideData(); + ChooserActivityOverrideData.getInstance().featureFlagRepository = + new TestFeatureFlagRepository(mFlags); } /** @@ -221,11 +251,13 @@ public class UnbundledChooserActivityTest { * -------- */ + @Rule + public final TestRule mRule; + // Shared test code references the activity under test as ChooserActivity, the common ancestor // of any (inheritance-based) chooser implementation. For testing purposes, that activity will // usually be cast to IChooserWrapper to expose instrumentation. - @Rule - public ActivityTestRule mActivityRule = + private ActivityTestRule mActivityRule = new ActivityTestRule<>(ChooserActivity.class, false, false) { @Override public ChooserActivity launchActivity(Intent clientIntent) { @@ -252,16 +284,20 @@ public class UnbundledChooserActivityTest { private static final int CONTENT_PREVIEW_IMAGE = 1; private static final int CONTENT_PREVIEW_FILE = 2; private static final int CONTENT_PREVIEW_TEXT = 3; - private Function mPackageManagerOverride; - private int mTestNum; + + private final Function mPackageManagerOverride; + private final Map mFlags; public UnbundledChooserActivityTest( - int testNum, - String testName, - Function packageManagerOverride) { + Function packageManagerOverride, + Map flags) { mPackageManagerOverride = packageManagerOverride; - mTestNum = testNum; + mFlags = flags; + + mRule = RuleChain + .outerRule(new FeatureFlagRule(flags)) + .around(mActivityRule); } private void setDeviceConfigProperty( @@ -757,10 +793,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) public void testImagePlusTextSharing_ExcludeText() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240)); @@ -809,10 +845,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) public void testImagePlusTextSharing_RemoveAndAddBackText() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240)); @@ -865,10 +901,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240)); @@ -1070,10 +1106,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, + values = { false }) public void twoVisibleImagePreview() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW, false)); Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); @@ -1110,11 +1146,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, + values = { false }) public void threeOrMoreVisibleImagePreview() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap( - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW, false)); Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); @@ -1154,11 +1189,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, + values = { true }) public void testManyVisibleImagePreview_ScrollableImagePreview() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap( - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW, true)); Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); @@ -1204,10 +1238,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) public void testImageAndTextPreview() { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, true)); final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); final String sharedText = "text-" + System.currentTimeMillis(); @@ -1943,10 +1977,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_CUSTOM_ACTIONS_NAME }, + values = { true }) public void testLaunchWithCustomAction() throws InterruptedException { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_CUSTOM_ACTIONS, true)); List resolvedComponentInfos = createResolvedComponentsForTest(2); when( ChooserActivityOverrideData @@ -1998,10 +2032,10 @@ public class UnbundledChooserActivityTest { } @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_RESELECTION_ACTION_NAME }, + values = { true }) public void testLaunchWithShareModification() throws InterruptedException { - ChooserActivityOverrideData.getInstance().featureFlagRepository = - new TestFeatureFlagRepository( - Collections.singletonMap(Flags.SHARESHEET_RESELECTION_ACTION, true)); List resolvedComponentInfos = createResolvedComponentsForTest(2); when( ChooserActivityOverrideData -- cgit v1.2.3-59-g8ed1b From 6f3ea1e9310afb2a56c1491148802e1b6154a094 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Mon, 13 Feb 2023 19:10:50 +0000 Subject: Extract ChooserActionFactory. This was a sizable chunk of code dedicated to one logical set of responsibilities, so it's nice to separate. Test: `atest IntentResolverUnitTests` Bug: 202167050 Change-Id: I3b033e975afeee66e33da38d0dc0eeba768d0ed4 --- .../intentresolver/ChooserActionFactory.java | 477 +++++++++++++++++++++ .../android/intentresolver/ChooserActivity.java | 403 ++--------------- .../ChooserIntegratedDeviceComponents.java | 77 ++++ .../intentresolver/ChooserRequestParameters.java | 20 +- .../intentresolver/ChooserWrapperActivity.java | 19 +- 5 files changed, 626 insertions(+), 370 deletions(-) create mode 100644 java/src/com/android/intentresolver/ChooserActionFactory.java create mode 100644 java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java new file mode 100644 index 00000000..1fe55890 --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -0,0 +1,477 @@ +/* + * 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; + +import android.annotation.Nullable; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.content.pm.ResolveInfo; +import android.content.res.Resources; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.service.chooser.ChooserAction; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; + +import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.flags.FeatureFlagRepository; +import com.android.intentresolver.flags.Flags; +import com.android.intentresolver.widget.ActionRow; +import com.android.internal.annotations.VisibleForTesting; + +import com.google.common.collect.ImmutableList; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application + * requirements of Sharesheet / {@link ChooserActivity}. + */ +public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory { + /** Delegate interface to launch activities when the actions are selected. */ + public interface ActionActivityStarter { + /** + * Request an activity launch for the provided target. Implementations may choose to exit + * the current activity when the target is launched. + */ + void safelyStartActivityAsPersonalProfileUser(TargetInfo info); + + /** + * Request an activity launch for the provided target, optionally employing the specified + * shared element transition. Implementations may choose to exit the current activity when + * the target is launched. + */ + default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( + TargetInfo info, View sharedElement, String sharedElementName) { + safelyStartActivityAsPersonalProfileUser(info); + } + } + + private static final String TAG = "ChooserActions"; + + private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION + | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; + + private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; + private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; + + private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image"; + + private final Context mContext; + private final String mCopyButtonLabel; + private final Drawable mCopyButtonDrawable; + private final Runnable mOnCopyButtonClicked; + private final TargetInfo mEditSharingTarget; + private final Runnable mOnEditButtonClicked; + private final TargetInfo mNearbySharingTarget; + private final Runnable mOnNearbyButtonClicked; + private final ImmutableList mCustomActions; + private final PendingIntent mReselectionIntent; + private final Consumer mExcludeSharedTextAction; + private final Consumer mFinishCallback; + + /** + * @param context + * @param chooserRequest data about the invocation of the current Sharesheet session. + * @param featureFlagRepository feature flags that may control the eligibility of some actions. + * @param integratedDeviceComponents info about other components that are available on this + * device to implement the supported action types. + * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text" + * setting is updated. The argument is whether the shared text is to be excluded. + * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image + * View in the Sharesheet UI, if any, or null. + * @param activityStarter a delegate to launch activities when actions are selected. + * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was + * completed). + */ + public ChooserActionFactory( + Context context, + ChooserRequestParameters chooserRequest, + FeatureFlagRepository featureFlagRepository, + ChooserIntegratedDeviceComponents integratedDeviceComponents, + ChooserActivityLogger logger, + Consumer onUpdateSharedTextIsExcluded, + Callable firstVisibleImageQuery, + ActionActivityStarter activityStarter, + Consumer finishCallback) { + this( + context, + context.getString(com.android.internal.R.string.copy), + context.getDrawable(com.android.internal.R.drawable.ic_menu_copy_material), + makeOnCopyRunnable( + context, + chooserRequest.getTargetIntent(), + chooserRequest.getReferrerPackageName(), + finishCallback, + logger), + getEditSharingTarget( + context, + chooserRequest.getTargetIntent(), + integratedDeviceComponents), + makeOnEditRunnable( + getEditSharingTarget( + context, + chooserRequest.getTargetIntent(), + integratedDeviceComponents), + firstVisibleImageQuery, + activityStarter, + logger), + getNearbySharingTarget( + context, + chooserRequest.getTargetIntent(), + integratedDeviceComponents), + makeOnNearbyShareRunnable( + getNearbySharingTarget( + context, + chooserRequest.getTargetIntent(), + integratedDeviceComponents), + activityStarter, + finishCallback, + logger), + chooserRequest.getChooserActions(), + (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) + ? chooserRequest.getModifyShareAction() : null), + onUpdateSharedTextIsExcluded, + finishCallback); + } + + @VisibleForTesting + ChooserActionFactory( + Context context, + String copyButtonLabel, + Drawable copyButtonDrawable, + Runnable onCopyButtonClicked, + TargetInfo editSharingTarget, + Runnable onEditButtonClicked, + TargetInfo nearbySharingTarget, + Runnable onNearbyButtonClicked, + List customActions, + @Nullable PendingIntent reselectionIntent, + Consumer onUpdateSharedTextIsExcluded, + Consumer finishCallback) { + mContext = context; + mCopyButtonLabel = copyButtonLabel; + mCopyButtonDrawable = copyButtonDrawable; + mOnCopyButtonClicked = onCopyButtonClicked; + mEditSharingTarget = editSharingTarget; + mOnEditButtonClicked = onEditButtonClicked; + mNearbySharingTarget = nearbySharingTarget; + mOnNearbyButtonClicked = onNearbyButtonClicked; + mCustomActions = ImmutableList.copyOf(customActions); + mReselectionIntent = reselectionIntent; + mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; + mFinishCallback = finishCallback; + } + + /** Create an action that copies the share content to the clipboard. */ + @Override + public ActionRow.Action createCopyButton() { + return new ActionRow.Action( + com.android.internal.R.id.chooser_copy_button, + mCopyButtonLabel, + mCopyButtonDrawable, + mOnCopyButtonClicked); + } + + /** Create an action that opens the share content in a system-default editor. */ + @Override + @Nullable + public ActionRow.Action createEditButton() { + if (mEditSharingTarget == null) { + return null; + } + + return new ActionRow.Action( + com.android.internal.R.id.chooser_edit_button, + mEditSharingTarget.getDisplayLabel(), + mEditSharingTarget.getDisplayIconHolder().getDisplayIcon(), + mOnEditButtonClicked); + } + + /** Create a "Share to Nearby" action. */ + @Override + @Nullable + public ActionRow.Action createNearbyButton() { + if (mNearbySharingTarget == null) { + return null; + } + + return new ActionRow.Action( + com.android.internal.R.id.chooser_nearby_button, + mNearbySharingTarget.getDisplayLabel(), + mNearbySharingTarget.getDisplayIconHolder().getDisplayIcon(), + mOnNearbyButtonClicked); + } + + /** Create custom actions */ + @Override + public List createCustomActions() { + return mCustomActions.stream() + .map(target -> createCustomAction(mContext, target, mFinishCallback)) + .filter(action -> action != null) + .collect(Collectors.toList()); + } + + /** + * Provides a share modification action, if any. + */ + @Override + @Nullable + public Runnable getModifyShareAction() { + return (mReselectionIntent == null) ? null : createReselectionRunnable(mReselectionIntent); + } + + private Runnable createReselectionRunnable(PendingIntent pendingIntent) { + return () -> { + try { + pendingIntent.send(); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, "Payload reselection action has been cancelled"); + } + // TODO: add reporting + mFinishCallback.accept(Activity.RESULT_OK); + }; + } + + /** + *

+ * Creates an exclude-text action that can be called when the user changes shared text + * status in the Media + Text preview. + *

+ *

+ * true argument value indicates that the text should be excluded. + *

+ */ + @Override + public Consumer getExcludeSharedTextAction() { + return mExcludeSharedTextAction; + } + + private static Runnable makeOnCopyRunnable( + Context context, + Intent targetIntent, + String referrerPackageName, + Consumer finishCallback, + ChooserActivityLogger logger) { + return () -> { + if (targetIntent == null) { + finishCallback.accept(null); + return; + } + + final String action = targetIntent.getAction(); + + ClipData clipData = null; + if (Intent.ACTION_SEND.equals(action)) { + String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); + Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); + + if (extraText != null) { + clipData = ClipData.newPlainText(null, extraText); + } else if (extraStream != null) { + clipData = ClipData.newUri(context.getContentResolver(), null, extraStream); + } else { + Log.w(TAG, "No data available to copy to clipboard"); + return; + } + } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { + final ArrayList streams = targetIntent.getParcelableArrayListExtra( + Intent.EXTRA_STREAM); + clipData = ClipData.newUri(context.getContentResolver(), null, streams.get(0)); + for (int i = 1; i < streams.size(); i++) { + clipData.addItem( + context.getContentResolver(), + new ClipData.Item(streams.get(i))); + } + } else { + // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE + // so warn about unexpected action + Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard"); + return; + } + + ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService( + Context.CLIPBOARD_SERVICE); + clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName); + + logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY); + finishCallback.accept(Activity.RESULT_OK); + }; + } + + private static TargetInfo getEditSharingTarget( + Context context, + Intent originalIntent, + ChooserIntegratedDeviceComponents integratedComponents) { + final ComponentName editorComponent = integratedComponents.getEditSharingComponent(); + + final Intent resolveIntent = new Intent(originalIntent); + // Retain only URI permission grant flags if present. Other flags may prevent the scene + // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION, + // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed. + resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS); + resolveIntent.setComponent(editorComponent); + resolveIntent.setAction(Intent.ACTION_EDIT); + String originalAction = originalIntent.getAction(); + if (Intent.ACTION_SEND.equals(originalAction)) { + if (resolveIntent.getData() == null) { + Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM); + if (uri != null) { + String mimeType = context.getContentResolver().getType(uri); + resolveIntent.setDataAndType(uri, mimeType); + } + } + } else { + Log.e(TAG, originalAction + " is not supported."); + return null; + } + final ResolveInfo ri = context.getPackageManager().resolveActivity( + resolveIntent, PackageManager.GET_META_DATA); + if (ri == null || ri.activityInfo == null) { + Log.e(TAG, "Device-specified editor (" + editorComponent + ") not available"); + return null; + } + + final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, + ri, + context.getString(com.android.internal.R.string.screenshot_edit), + "", + resolveIntent, + null); + dri.getDisplayIconHolder().setDisplayIcon( + context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit)); + return dri; + } + + private static Runnable makeOnEditRunnable( + TargetInfo editSharingTarget, + Callable firstVisibleImageQuery, + ActionActivityStarter activityStarter, + ChooserActivityLogger logger) { + return () -> { + // Log share completion via edit. + logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT); + + View firstImageView = null; + try { + firstImageView = firstVisibleImageQuery.call(); + } catch (Exception e) { /* ignore */ } + // Action bar is user-independent; always start as primary. + if (firstImageView == null) { + activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget); + } else { + activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( + editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT); + } + }; + } + + private static TargetInfo getNearbySharingTarget( + Context context, + Intent originalIntent, + ChooserIntegratedDeviceComponents integratedComponents) { + final ComponentName cn = integratedComponents.getNearbySharingComponent(); + if (cn == null) return null; + + final Intent resolveIntent = new Intent(originalIntent); + resolveIntent.setComponent(cn); + final ResolveInfo ri = context.getPackageManager().resolveActivity( + resolveIntent, PackageManager.GET_META_DATA); + if (ri == null || ri.activityInfo == null) { + Log.e(TAG, "Device-specified nearby sharing component (" + cn + + ") not available"); + return null; + } + + // Allow the nearby sharing component to provide a more appropriate icon and label + // for the chip. + CharSequence name = null; + Drawable icon = null; + final Bundle metaData = ri.activityInfo.metaData; + if (metaData != null) { + try { + final Resources pkgRes = context.getPackageManager().getResourcesForActivity(cn); + final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY); + name = pkgRes.getString(nameResId); + final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY); + icon = pkgRes.getDrawable(resId); + } catch (NameNotFoundException | Resources.NotFoundException ex) { /* ignore */ } + } + if (TextUtils.isEmpty(name)) { + name = ri.loadLabel(context.getPackageManager()); + } + if (icon == null) { + icon = ri.loadIcon(context.getPackageManager()); + } + + final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( + originalIntent, ri, name, "", resolveIntent, null); + dri.getDisplayIconHolder().setDisplayIcon(icon); + return dri; + } + + private static Runnable makeOnNearbyShareRunnable( + TargetInfo nearbyShareTarget, + ActionActivityStarter activityStarter, + Consumer finishCallback, + ChooserActivityLogger logger) { + return () -> { + logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_NEARBY); + // Action bar is user-independent; always start as primary. + activityStarter.safelyStartActivityAsPersonalProfileUser(nearbyShareTarget); + }; + } + + @Nullable + private static ActionRow.Action createCustomAction( + Context context, ChooserAction action, Consumer finishCallback) { + Drawable icon = action.getIcon().loadDrawable(context); + if (icon == null && TextUtils.isEmpty(action.getLabel())) { + return null; + } + return new ActionRow.Action( + action.getLabel(), + icon, + () -> { + try { + action.getAction().send(); + } catch (PendingIntent.CanceledException e) { + Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled"); + } + // TODO: add reporting + finishCallback.accept(Activity.RESULT_OK); + } + ); + } +} diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 34390770..a2f2bbde 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -32,13 +32,10 @@ import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; import android.app.ActivityOptions; -import android.app.PendingIntent; import android.app.prediction.AppPredictor; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -49,31 +46,24 @@ import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.content.pm.ShortcutInfo; import android.content.res.Configuration; -import android.content.res.Resources; import android.database.Cursor; import android.graphics.Insets; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; -import android.os.PatternMatcher; import android.os.ResultReceiver; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; import android.provider.DeviceConfig; -import android.provider.Settings; -import android.service.chooser.ChooserAction; import android.service.chooser.ChooserTarget; -import android.text.TextUtils; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -100,7 +90,6 @@ import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.grid.DirectShareViewHolder; import com.android.intentresolver.model.AbstractResolverComparator; @@ -108,7 +97,6 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; @@ -116,8 +104,6 @@ import com.android.internal.content.PackageMonitor; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.util.FrameworkStatsLog; -import com.google.common.collect.ImmutableList; - import java.io.File; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -210,6 +196,8 @@ public class ChooserActivity extends ResolverActivity implements | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; + private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; + /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the * only assignment there, and expect it to be ready by the time we ever use it -- * someday if we move all the usage to a component with a narrower lifecycle (something that @@ -220,6 +208,7 @@ public class ChooserActivity extends ResolverActivity implements private ChooserRequestParameters mChooserRequest; private FeatureFlagRepository mFeatureFlagRepository; + private ChooserActionFactory mChooserActionFactory; private ChooserContentPreviewUi mChooserContentPreviewUi; private boolean mShouldDisplayLandscape; @@ -274,11 +263,14 @@ public class ChooserActivity extends ResolverActivity implements getChooserActivityLogger().logSharesheetTriggered(); mFeatureFlagRepository = createFeatureFlagRepository(); + mIntegratedDeviceComponents = getIntegratedDeviceComponents(); + try { mChooserRequest = new ChooserRequestParameters( getIntent(), + getReferrerPackageName(), getReferrer(), - getNearbySharingComponent(), + mIntegratedDeviceComponents, mFeatureFlagRepository); } catch (IllegalArgumentException e) { Log.e(TAG, "Caller provided invalid Chooser request parameters", e); @@ -286,6 +278,39 @@ public class ChooserActivity extends ResolverActivity implements super_onCreate(null); return; } + + mChooserActionFactory = new ChooserActionFactory( + this, + mChooserRequest, + mFeatureFlagRepository, + mIntegratedDeviceComponents, + getChooserActivityLogger(), + (isExcluded) -> mExcludeSharedText = isExcluded, + this::getFirstVisibleImgPreviewView, + new ChooserActionFactory.ActionActivityStarter() { + @Override + public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { + safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle()); + finish(); + } + + @Override + public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( + TargetInfo targetInfo, View sharedElement, String sharedElementName) { + ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( + ChooserActivity.this, sharedElement, sharedElementName); + safelyStartActivityAsUser( + targetInfo, getPersonalProfileUserHandle(), options.toBundle()); + startFinishAnimation(); + } + }, + (status) -> { + if (status != null) { + setResult(status); + } + finish(); + }); + mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); @@ -368,6 +393,11 @@ public class ChooserActivity extends ResolverActivity implements mEnterTransitionAnimationDelegate.postponeTransition(); } + @VisibleForTesting + protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { + return ChooserIntegratedDeviceComponents.get(this); + } + @Override protected int appliedThemeResId() { return R.style.Theme_DeviceDefault_Chooser; @@ -607,51 +637,6 @@ public class ChooserActivity extends ResolverActivity implements updateProfileViewButton(); } - private void onCopyButtonClicked() { - Intent targetIntent = getTargetIntent(); - if (targetIntent == null) { - finish(); - } else { - final String action = targetIntent.getAction(); - - ClipData clipData = null; - if (Intent.ACTION_SEND.equals(action)) { - String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT); - Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); - - if (extraText != null) { - clipData = ClipData.newPlainText(null, extraText); - } else if (extraStream != null) { - clipData = ClipData.newUri(getContentResolver(), null, extraStream); - } else { - Log.w(TAG, "No data available to copy to clipboard"); - return; - } - } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) { - final ArrayList streams = targetIntent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM); - clipData = ClipData.newUri(getContentResolver(), null, streams.get(0)); - for (int i = 1; i < streams.size(); i++) { - clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i))); - } - } else { - // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE - // so warn about unexpected action - Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard"); - return; - } - - ClipboardManager clipboardManager = (ClipboardManager) getSystemService( - Context.CLIPBOARD_SERVICE); - clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName()); - - getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY); - - setResult(RESULT_OK); - finish(); - } - } - @Override protected void onResume() { super.onResume(); @@ -728,64 +713,12 @@ public class ChooserActivity extends ResolverActivity implements int previewType = ChooserContentPreviewUi.findPreferredContentPreview( targetIntent, getContentResolver(), this::isImageType); - ChooserContentPreviewUi.ActionFactory actionFactory = - new ChooserContentPreviewUi.ActionFactory() { - @Override - public ActionRow.Action createCopyButton() { - return ChooserActivity.this.createCopyAction(); - } - - @Nullable - @Override - public ActionRow.Action createEditButton() { - return ChooserActivity.this.createEditAction(targetIntent); - } - - @Nullable - @Override - public ActionRow.Action createNearbyButton() { - return ChooserActivity.this.createNearbyAction(targetIntent); - } - - @Override - public List createCustomActions() { - ImmutableList customActions = - mChooserRequest.getChooserActions(); - List actions = new ArrayList<>(customActions.size()); - for (ChooserAction customAction : customActions) { - ActionRow.Action action = createCustomAction(customAction); - if (action != null) { - actions.add(action); - } - } - return actions; - } - - @Nullable - @Override - public Runnable getModifyShareAction() { - if (!mFeatureFlagRepository - .isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) { - return null; - } - PendingIntent reselectionAction = mChooserRequest.getModifyShareAction(); - return reselectionAction == null - ? null - : createReselectionRunnable(reselectionAction); - } - - @Override - public Consumer getExcludeSharedTextAction() { - return (isExcluded) -> mExcludeSharedText = isExcluded; - } - }; - ViewGroup layout = mChooserContentPreviewUi.displayContentPreview( previewType, targetIntent, getResources(), getLayoutInflater(), - actionFactory, + mChooserActionFactory, parent, imageLoader, mEnterTransitionAnimationDelegate, @@ -799,211 +732,6 @@ public class ChooserActivity extends ResolverActivity implements return layout; } - @VisibleForTesting - protected ComponentName getNearbySharingComponent() { - String nearbyComponent = Settings.Secure.getString( - getContentResolver(), - Settings.Secure.NEARBY_SHARING_COMPONENT); - if (TextUtils.isEmpty(nearbyComponent)) { - nearbyComponent = getString(R.string.config_defaultNearbySharingComponent); - } - if (TextUtils.isEmpty(nearbyComponent)) { - return null; - } - return ComponentName.unflattenFromString(nearbyComponent); - } - - @VisibleForTesting - protected @Nullable ComponentName getEditSharingComponent() { - String editorPackage = getApplicationContext().getString(R.string.config_systemImageEditor); - if (editorPackage == null || TextUtils.isEmpty(editorPackage)) { - return null; - } - return ComponentName.unflattenFromString(editorPackage); - } - - @VisibleForTesting - protected TargetInfo getEditSharingTarget(Intent originalIntent) { - final ComponentName cn = getEditSharingComponent(); - - final Intent resolveIntent = new Intent(originalIntent); - // Retain only URI permission grant flags if present. Other flags may prevent the scene - // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION, - // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed. - resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS); - resolveIntent.setComponent(cn); - resolveIntent.setAction(Intent.ACTION_EDIT); - String originalAction = originalIntent.getAction(); - if (Intent.ACTION_SEND.equals(originalAction)) { - if (resolveIntent.getData() == null) { - Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM); - if (uri != null) { - String mimeType = getContentResolver().getType(uri); - resolveIntent.setDataAndType(uri, mimeType); - } - } - } else { - Log.e(TAG, originalAction + " is not supported."); - return null; - } - final ResolveInfo ri = getPackageManager().resolveActivity( - resolveIntent, PackageManager.GET_META_DATA); - if (ri == null || ri.activityInfo == null) { - Log.e(TAG, "Device-specified image edit component (" + cn - + ") not available"); - return null; - } - - final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( - originalIntent, - ri, - getString(com.android.internal.R.string.screenshot_edit), - "", - resolveIntent, - null); - dri.getDisplayIconHolder().setDisplayIcon( - getDrawable(com.android.internal.R.drawable.ic_screenshot_edit)); - return dri; - } - - @VisibleForTesting - protected TargetInfo getNearbySharingTarget(Intent originalIntent) { - final ComponentName cn = getNearbySharingComponent(); - if (cn == null) return null; - - final Intent resolveIntent = new Intent(originalIntent); - resolveIntent.setComponent(cn); - final ResolveInfo ri = getPackageManager().resolveActivity( - resolveIntent, PackageManager.GET_META_DATA); - if (ri == null || ri.activityInfo == null) { - Log.e(TAG, "Device-specified nearby sharing component (" + cn - + ") not available"); - return null; - } - - // Allow the nearby sharing component to provide a more appropriate icon and label - // for the chip. - CharSequence name = null; - Drawable icon = null; - final Bundle metaData = ri.activityInfo.metaData; - if (metaData != null) { - try { - final Resources pkgRes = getPackageManager().getResourcesForActivity(cn); - final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY); - name = pkgRes.getString(nameResId); - final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY); - icon = pkgRes.getDrawable(resId); - } catch (Resources.NotFoundException ex) { - } catch (NameNotFoundException ex) { - } - } - if (TextUtils.isEmpty(name)) { - name = ri.loadLabel(getPackageManager()); - } - if (icon == null) { - icon = ri.loadIcon(getPackageManager()); - } - - final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( - originalIntent, ri, name, "", resolveIntent, null); - dri.getDisplayIconHolder().setDisplayIcon(icon); - return dri; - } - - private ActionRow.Action createCopyAction() { - return new ActionRow.Action( - com.android.internal.R.id.chooser_copy_button, - getString(com.android.internal.R.string.copy), - getDrawable(com.android.internal.R.drawable.ic_menu_copy_material), - this::onCopyButtonClicked); - } - - @Nullable - private ActionRow.Action createNearbyAction(Intent originalIntent) { - final TargetInfo ti = getNearbySharingTarget(originalIntent); - if (ti == null) { - return null; - } - - return new ActionRow.Action( - com.android.internal.R.id.chooser_nearby_button, - ti.getDisplayLabel(), - ti.getDisplayIconHolder().getDisplayIcon(), - () -> { - getChooserActivityLogger().logActionSelected( - ChooserActivityLogger.SELECTION_TYPE_NEARBY); - // Action bar is user-independent, always start as primary - safelyStartActivityAsUser(ti, getPersonalProfileUserHandle()); - finish(); - }); - } - - @Nullable - private ActionRow.Action createEditAction(Intent originalIntent) { - final TargetInfo ti = getEditSharingTarget(originalIntent); - if (ti == null) { - return null; - } - - return new ActionRow.Action( - com.android.internal.R.id.chooser_edit_button, - ti.getDisplayLabel(), - ti.getDisplayIconHolder().getDisplayIcon(), - () -> { - // Log share completion via edit - getChooserActivityLogger().logActionSelected( - ChooserActivityLogger.SELECTION_TYPE_EDIT); - View firstImgView = getFirstVisibleImgPreviewView(); - // Action bar is user-independent, always start as primary - if (firstImgView == null) { - safelyStartActivityAsUser(ti, getPersonalProfileUserHandle()); - finish(); - } else { - ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( - this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT); - safelyStartActivityAsUser( - ti, getPersonalProfileUserHandle(), options.toBundle()); - startFinishAnimation(); - } - } - ); - } - - @Nullable - private ActionRow.Action createCustomAction(ChooserAction action) { - Drawable icon = action.getIcon().loadDrawable(this); - if (icon == null && TextUtils.isEmpty(action.getLabel())) { - return null; - } - return new ActionRow.Action( - action.getLabel(), - icon, - () -> { - try { - action.getAction().send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled"); - } - // TODO: add reporting - setResult(RESULT_OK); - finish(); - } - ); - } - - private Runnable createReselectionRunnable(PendingIntent pendingIntent) { - return () -> { - try { - pendingIntent.send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "Payload reselection action has been cancelled"); - } - // TODO: add reporting - setResult(RESULT_OK); - finish(); - }; - } - @Nullable private View getFirstVisibleImgPreviewView() { View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large); @@ -1315,45 +1043,6 @@ public class ChooserActivity extends ResolverActivity implements } } - private IntentFilter getTargetIntentFilter() { - return getTargetIntentFilter(getTargetIntent()); - } - - private IntentFilter getTargetIntentFilter(final Intent intent) { - try { - String dataString = intent.getDataString(); - if (intent.getType() == null) { - if (!TextUtils.isEmpty(dataString)) { - return new IntentFilter(intent.getAction(), dataString); - } - Log.e(TAG, "Failed to get target intent filter: intent data and type are null"); - return null; - } - IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType()); - List contentUris = new ArrayList<>(); - if (Intent.ACTION_SEND.equals(intent.getAction())) { - Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); - if (uri != null) { - contentUris.add(uri); - } - } else { - List uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - if (uris != null) { - contentUris.addAll(uris); - } - } - for (Uri uri : contentUris) { - intentFilter.addDataScheme(uri.getScheme()); - intentFilter.addDataAuthority(uri.getAuthority(), null); - intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL); - } - return intentFilter; - } catch (Exception e) { - Log.e(TAG, "Failed to get target intent filter", e); - return null; - } - } - private void logDirectShareTargetReceived(UserHandle forUser) { ProfileRecord profileRecord = getProfileRecord(forUser); if (profileRecord == null) { diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java new file mode 100644 index 00000000..9b124c20 --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java @@ -0,0 +1,77 @@ +/* + * 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; + +import android.annotation.Nullable; +import android.content.ComponentName; +import android.content.Context; +import android.provider.Settings; +import android.text.TextUtils; + +import com.android.internal.annotations.VisibleForTesting; + +/** + * Helper to look up the components available on this device to handle assorted built-in actions + * like "Edit" that may be displayed for certain content/preview types. The components are queried + * when this record is instantiated, and are then immutable for a given instance. + * + * Because this describes the app's external execution environment, test methods may prefer to + * provide explicit values to override the default lookup logic. + */ +public final class ChooserIntegratedDeviceComponents { + @Nullable + private final ComponentName mEditSharingComponent; + + @Nullable + private final ComponentName mNearbySharingComponent; + + /** Look up the integrated components available on this device. */ + public static ChooserIntegratedDeviceComponents get(Context context) { + return new ChooserIntegratedDeviceComponents( + getEditSharingComponent(context), + getNearbySharingComponent(context)); + } + + @VisibleForTesting + ChooserIntegratedDeviceComponents( + ComponentName editSharingComponent, ComponentName nearbySharingComponent) { + mEditSharingComponent = editSharingComponent; + mNearbySharingComponent = nearbySharingComponent; + } + + public ComponentName getEditSharingComponent() { + return mEditSharingComponent; + } + + public ComponentName getNearbySharingComponent() { + return mNearbySharingComponent; + } + + private static ComponentName getEditSharingComponent(Context context) { + String editorComponent = context.getApplicationContext().getString( + R.string.config_systemImageEditor); + return TextUtils.isEmpty(editorComponent) + ? null : ComponentName.unflattenFromString(editorComponent); + } + + private static ComponentName getNearbySharingComponent(Context context) { + String nearbyComponent = Settings.Secure.getString( + context.getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT); + return TextUtils.isEmpty(nearbyComponent) + ? null : ComponentName.unflattenFromString(nearbyComponent); + } +} diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 2b67b273..83a0e2e1 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -71,6 +71,8 @@ public class ChooserRequestParameters { Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; private final Intent mTarget; + private final ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; + private final String mReferrerPackageName; private final Pair mTitleSpec; private final Intent mReferrerFillInIntent; private final ImmutableList mFilteredComponentNames; @@ -102,13 +104,18 @@ public class ChooserRequestParameters { public ChooserRequestParameters( final Intent clientIntent, + String referrerPackageName, final Uri referrer, - @Nullable final ComponentName nearbySharingComponent, + ChooserIntegratedDeviceComponents integratedDeviceComponents, FeatureFlagRepository featureFlags) { final Intent requestedTarget = parseTargetIntentExtra( clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); mTarget = intentWithModifiedLaunchFlags(requestedTarget); + mIntegratedDeviceComponents = integratedDeviceComponents; + + mReferrerPackageName = referrerPackageName; + mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent( clientIntent, Intent.EXTRA_ALTERNATE_INTENTS); @@ -128,7 +135,8 @@ public class ChooserRequestParameters { mRefinementIntentSender = clientIntent.getParcelableExtra( Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); - mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent); + mFilteredComponentNames = getFilteredComponentNames( + clientIntent, mIntegratedDeviceComponents.getNearbySharingComponent()); mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent); @@ -165,6 +173,10 @@ public class ChooserRequestParameters { return getTargetIntent().getType(); } + public String getReferrerPackageName() { + return mReferrerPackageName; + } + @Nullable public CharSequence getTitle() { return mTitleSpec.first; @@ -245,6 +257,10 @@ public class ChooserRequestParameters { return mTargetIntentFilter; } + public ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { + return mIntegratedDeviceComponents; + } + private static boolean isSendAction(@Nullable String action) { return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)); } diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index a47014e8..17084e1c 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -37,7 +37,6 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileI import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager; import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.chooser.NotSelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; @@ -120,15 +119,13 @@ public class ChooserWrapperActivity } @Override - protected ComponentName getNearbySharingComponent() { - // an arbitrary pre-installed activity that handles this type of intent - return ComponentName.unflattenFromString("com.google.android.apps.messaging/" - + "com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity"); - } - - @Override - protected TargetInfo getNearbySharingTarget(Intent originalIntent) { - return NotSelectableTargetInfo.newEmptyTargetInfo(); + protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { + return new ChooserIntegratedDeviceComponents( + /* editSharingComponent=*/ null, + // An arbitrary pre-installed activity that handles this type of intent: + /* nearbySharingComponent=*/ new ComponentName( + "com.google.android.apps.messaging", + ".ui.conversationlist.ShareIntentActivity")); } @Override @@ -172,7 +169,7 @@ public class ChooserWrapperActivity } @Override - public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) { + public void safelyStartActivity(TargetInfo cti) { if (sOverrides.onSafelyStartCallback != null && sOverrides.onSafelyStartCallback.apply(cti)) { return; -- cgit v1.2.3-59-g8ed1b From c07d3f064db9cf715e36e9b6d4c1cd516e2258ce Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Mon, 13 Feb 2023 23:38:29 +0000 Subject: Extract a component to handle refinement. This is in advance of any possible bug-fixes related to b/262805893 (which may probably be accompanied by additional unit tests..) Test: `atest IntentResolverUnitTests` Bug: 202167050 Change-Id: I4c8d20522236559ff99b6e11a7c1a3a0fcbbd17d --- .../android/intentresolver/ChooserActivity.java | 172 ++--------------- .../intentresolver/ChooserRefinementManager.java | 215 +++++++++++++++++++++ 2 files changed, 236 insertions(+), 151 deletions(-) create mode 100644 java/src/com/android/intentresolver/ChooserRefinementManager.java (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index a2f2bbde..65c72fda 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -42,7 +42,6 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; -import android.content.IntentSender.SendIntentException; import android.content.SharedPreferences; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; @@ -54,10 +53,6 @@ import android.graphics.Insets; import android.net.Uri; import android.os.Bundle; import android.os.Environment; -import android.os.Handler; -import android.os.Parcel; -import android.os.Parcelable; -import android.os.ResultReceiver; import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; @@ -207,6 +202,8 @@ public class ChooserActivity extends ResolverActivity implements @Nullable private ChooserRequestParameters mChooserRequest; + private ChooserRefinementManager mRefinementManager; + private FeatureFlagRepository mFeatureFlagRepository; private ChooserActionFactory mChooserActionFactory; private ChooserContentPreviewUi mChooserContentPreviewUi; @@ -215,9 +212,6 @@ public class ChooserActivity extends ResolverActivity implements // statsd logger wrapper protected ChooserActivityLogger mChooserActivityLogger; - @Nullable - private RefinementResultReceiver mRefinementResultReceiver; - private long mChooserShownTime; protected boolean mIsSuccessfullySelected; @@ -311,6 +305,20 @@ public class ChooserActivity extends ResolverActivity implements finish(); }); + mRefinementManager = new ChooserRefinementManager( + this, + mChooserRequest.getRefinementIntentSender(), + (validatedRefinedTarget) -> { + maybeRemoveSharedText(validatedRefinedTarget); + if (super.onTargetSelected(validatedRefinedTarget, false)) { + finish(); + } + }, + () -> { + mRefinementManager.destroy(); + finish(); + }); + mChooserContentPreviewUi = new ChooserContentPreviewUi(mFeatureFlagRepository); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); @@ -777,9 +785,9 @@ public class ChooserActivity extends ResolverActivity implements mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); } - if (mRefinementResultReceiver != null) { - mRefinementResultReceiver.destroy(); - mRefinementResultReceiver = null; + if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip? + mRefinementManager.destroy(); + mRefinementManager = null; } mBackgroundThreadPoolExecutor.shutdownNow(); @@ -903,32 +911,8 @@ public class ChooserActivity extends ResolverActivity implements @Override protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) { - if (mChooserRequest.getRefinementIntentSender() != null) { - final Intent fillIn = new Intent(); - final List sourceIntents = target.getAllSourceIntents(); - if (!sourceIntents.isEmpty()) { - fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); - if (sourceIntents.size() > 1) { - final Intent[] alts = new Intent[sourceIntents.size() - 1]; - for (int i = 1, N = sourceIntents.size(); i < N; i++) { - alts[i - 1] = sourceIntents.get(i); - } - fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts); - } - if (mRefinementResultReceiver != null) { - mRefinementResultReceiver.destroy(); - } - mRefinementResultReceiver = new RefinementResultReceiver(this, target, null); - fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, - mRefinementResultReceiver.copyForSending()); - try { - mChooserRequest.getRefinementIntentSender().sendIntent( - this, 0, fillIn, null, null); - return false; - } catch (SendIntentException e) { - Log.e(TAG, "Refinement IntentSender failed to send", e); - } - } + if (mRefinementManager.maybeHandleSelection(target)) { + return false; } updateModelAndChooserCounts(target); maybeRemoveSharedText(target); @@ -1157,47 +1141,6 @@ public class ChooserActivity extends ResolverActivity implements return (record == null) ? null : record.appPredictor; } - void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) { - if (mRefinementResultReceiver != null) { - mRefinementResultReceiver.destroy(); - mRefinementResultReceiver = null; - } - if (selectedTarget == null) { - Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); - } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) { - Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget - + " cannot match refined source intent " + matchingIntent); - } else { - TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0); - maybeRemoveSharedText(clonedTarget); - if (super.onTargetSelected(clonedTarget, false)) { - updateModelAndChooserCounts(clonedTarget); - finish(); - return; - } - } - onRefinementCanceled(); - } - - void onRefinementCanceled() { - if (mRefinementResultReceiver != null) { - mRefinementResultReceiver.destroy(); - mRefinementResultReceiver = null; - } - finish(); - } - - boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) { - final List targetIntents = target.getAllSourceIntents(); - for (int i = 0, N = targetIntents.size(); i < N; i++) { - final Intent targetIntent = targetIntents.get(i); - if (targetIntent.filterEquals(matchingIntent)) { - return true; - } - } - return false; - } - /** * Sort intents alphabetically based on display label. */ @@ -1892,79 +1835,6 @@ public class ChooserActivity extends ResolverActivity implements } } - static class ChooserTargetRankingInfo { - public final List scores; - public final UserHandle userHandle; - - ChooserTargetRankingInfo(List chooserTargetScores, - UserHandle userHandle) { - this.scores = chooserTargetScores; - this.userHandle = userHandle; - } - } - - static class RefinementResultReceiver extends ResultReceiver { - private ChooserActivity mChooserActivity; - private TargetInfo mSelectedTarget; - - public RefinementResultReceiver(ChooserActivity host, TargetInfo target, - Handler handler) { - super(handler); - mChooserActivity = host; - mSelectedTarget = target; - } - - @Override - protected void onReceiveResult(int resultCode, Bundle resultData) { - if (mChooserActivity == null) { - Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); - return; - } - if (resultData == null) { - Log.e(TAG, "RefinementResultReceiver received null resultData"); - return; - } - - switch (resultCode) { - case RESULT_CANCELED: - mChooserActivity.onRefinementCanceled(); - break; - case RESULT_OK: - Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); - if (intentParcelable instanceof Intent) { - mChooserActivity.onRefinementResult(mSelectedTarget, - (Intent) intentParcelable); - } else { - Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent" - + " in resultData with key Intent.EXTRA_INTENT"); - } - break; - default: - Log.w(TAG, "Unknown result code " + resultCode - + " sent to RefinementResultReceiver"); - break; - } - } - - public void destroy() { - mChooserActivity = null; - mSelectedTarget = null; - } - - /** - * Apps can't load this class directly, so we need a regular ResultReceiver copy for - * sending. Obtain this by parceling and unparceling (one weird trick). - */ - ResultReceiver copyForSending() { - Parcel parcel = Parcel.obtain(); - writeToParcel(parcel, 0); - parcel.setDataPosition(0); - ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel); - parcel.recycle(); - return receiverForSending; - } - } - /** * Used in combination with the scene transition when launching the image editor */ diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java new file mode 100644 index 00000000..5997bfed --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -0,0 +1,215 @@ +/* + * 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; + +import android.annotation.Nullable; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.IntentSender; +import android.content.IntentSender.SendIntentException; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ResultReceiver; +import android.util.Log; + +import com.android.intentresolver.chooser.TargetInfo; + +import java.util.List; +import java.util.function.Consumer; + +/** + * Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement + * activity" that will be invoked when a target is selected, allowing the calling app to add + * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to + * convert the format of the payload, or lazy-download some data that was deferred in the original + * call). + * + * TODO(b/262805893): this currently requires the result to be a refinement of the best + * match for the user's selected target among the initially-provided source intents (according to + * their originally-provided priority order). In order to support alternate formats/actions, we + * should instead require it to refine any of the source intents -- presumably, the first + * in priority order that matches according to {@link Intent#filterEquals()}. + */ +public final class ChooserRefinementManager { + private static final String TAG = "ChooserRefinement"; + + @Nullable + private final IntentSender mRefinementIntentSender; + + private final Context mContext; + private final Consumer mOnSelectionRefined; + private final Runnable mOnRefinementCancelled; + + @Nullable + private RefinementResultReceiver mRefinementResultReceiver; + + public ChooserRefinementManager( + Context context, + @Nullable IntentSender refinementIntentSender, + Consumer onSelectionRefined, + Runnable onRefinementCancelled) { + mContext = context; + mRefinementIntentSender = refinementIntentSender; + mOnSelectionRefined = onSelectionRefined; + mOnRefinementCancelled = onRefinementCancelled; + } + + /** + * Delegate the user's {@code selectedTarget} to the refinement flow, if possible. + * @return true if the selection should wait for a now-started refinement flow, or false if it + * can proceed by the default (non-refinement) logic. + */ + public boolean maybeHandleSelection(TargetInfo selectedTarget) { + if (mRefinementIntentSender == null) { + return false; + } + if (selectedTarget.getAllSourceIntents().isEmpty()) { + return false; + } + + destroy(); // Terminate any prior sessions. + mRefinementResultReceiver = new RefinementResultReceiver( + refinedIntent -> { + destroy(); + TargetInfo refinedTarget = getValidRefinedTarget(selectedTarget, refinedIntent); + if (refinedTarget != null) { + mOnSelectionRefined.accept(refinedTarget); + } else { + mOnRefinementCancelled.run(); + } + }, + mOnRefinementCancelled); + + Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget); + try { + mRefinementIntentSender.sendIntent(mContext, 0, refinementRequest, null, null); + return true; + } catch (SendIntentException e) { + Log.e(TAG, "Refinement IntentSender failed to send", e); + } + return false; + } + + /** Clean up any ongoing refinement session. */ + public void destroy() { + if (mRefinementResultReceiver != null) { + mRefinementResultReceiver.destroy(); + mRefinementResultReceiver = null; + } + } + + private static Intent makeRefinementRequest( + RefinementResultReceiver resultReceiver, TargetInfo originalTarget) { + final Intent fillIn = new Intent(); + final List sourceIntents = originalTarget.getAllSourceIntents(); + fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0)); + if (sourceIntents.size() > 1) { + fillIn.putExtra( + Intent.EXTRA_ALTERNATE_INTENTS, + sourceIntents.subList(1, sourceIntents.size()).toArray()); + } + fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, resultReceiver.copyForSending()); + return fillIn; + } + + private static class RefinementResultReceiver extends ResultReceiver { + private final Consumer mOnSelectionRefined; + private final Runnable mOnRefinementCancelled; + + private boolean mDestroyed; + + RefinementResultReceiver( + Consumer onSelectionRefined, + Runnable onRefinementCancelled) { + super(/* handler=*/ null); + mOnSelectionRefined = onSelectionRefined; + mOnRefinementCancelled = onRefinementCancelled; + } + + public void destroy() { + mDestroyed = true; + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (mDestroyed) { + Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); + return; + } + if (resultData == null) { + Log.e(TAG, "RefinementResultReceiver received null resultData"); + // TODO: treat as cancellation? + return; + } + + switch (resultCode) { + case Activity.RESULT_CANCELED: + mOnRefinementCancelled.run(); + break; + case Activity.RESULT_OK: + Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); + if (intentParcelable instanceof Intent) { + mOnSelectionRefined.accept((Intent) intentParcelable); + } else { + Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data"); + } + break; + default: + Log.w(TAG, "Received unknown refinement result " + resultCode); + break; + } + } + + /** + * Apps can't load this class directly, so we need a regular ResultReceiver copy for + * sending. Obtain this by parceling and unparceling (one weird trick). + */ + ResultReceiver copyForSending() { + Parcel parcel = Parcel.obtain(); + writeToParcel(parcel, 0); + parcel.setDataPosition(0); + ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel); + parcel.recycle(); + return receiverForSending; + } + } + + private static TargetInfo getValidRefinedTarget( + TargetInfo originalTarget, Intent proposedRefinement) { + if (originalTarget == null) { + // TODO: this legacy log message doesn't seem to describe the real condition we just + // checked; probably this method should never be invoked with a null target. + Log.e(TAG, "Refinement result intent did not match any known targets; canceling"); + return null; + } + if (!checkProposalRefinesSourceIntent(originalTarget, proposedRefinement)) { + Log.e(TAG, "Refinement " + proposedRefinement + " has no match in " + originalTarget); + return null; + } + return originalTarget.cloneFilledIn(proposedRefinement, 0); // TODO: select the right base. + } + + // TODO: return the actual match, to use as the base that we fill in? Or, if that's handled by + // `TargetInfo.cloneFilledIn()`, just let it be nullable (it already is?) and don't bother doing + // this pre-check. + private static boolean checkProposalRefinesSourceIntent( + TargetInfo originalTarget, Intent proposedMatch) { + return originalTarget.getAllSourceIntents().stream().anyMatch(proposedMatch::filterEquals); + } +} -- cgit v1.2.3-59-g8ed1b From cbfbfbcbf3c22331a3c1872e7ae987a95fe16e4c Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Wed, 15 Feb 2023 14:59:28 -0500 Subject: Prevent sharesheet from previewing unowned URIs This is a high priority security fix. Bug: 261036568 Test: manually via supplied tool (see bug) Change-Id: I7e05506dc260d10984b8e56a8e657b50177ff04d --- .../intentresolver/ChooserContentPreviewUi.java | 28 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java index 7d627e07..61affdf3 100644 --- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java @@ -16,6 +16,8 @@ package com.android.intentresolver; +import static android.content.ContentProvider.getUserIdFromUri; + import static java.lang.annotation.RetentionPolicy.SOURCE; import android.animation.ObjectAnimator; @@ -28,6 +30,7 @@ import android.content.res.Resources; import android.database.Cursor; import android.graphics.Bitmap; import android.net.Uri; +import android.os.UserHandle; import android.provider.DocumentsContract; import android.provider.Downloads; import android.provider.OpenableColumns; @@ -341,7 +344,7 @@ public final class ChooserContentPreviewUi { ImageView previewThumbnailView = contentPreviewLayout.findViewById( com.android.internal.R.id.content_preview_thumbnail); - if (previewThumbnail == null) { + if (!validForContentPreview(previewThumbnail)) { previewThumbnailView.setVisibility(View.GONE); } else { previewImageLoader.loadImage( @@ -538,14 +541,14 @@ public final class ChooserContentPreviewUi { List uris = new ArrayList<>(); if (Intent.ACTION_SEND.equals(targetIntent.getAction())) { Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); - if (uri != null) { + if (validForContentPreview(uri)) { uris.add(uri); } } else { List receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); if (receivedUris != null) { for (Uri uri : receivedUris) { - if (uri != null) { + if (validForContentPreview(uri)) { uris.add(uri); } } @@ -554,6 +557,25 @@ public final class ChooserContentPreviewUi { return uris; } + /** + * Indicate if the incoming content URI should be allowed. + * + * @param uri the uri to test + * @return true if the URI is allowed for content preview + */ + private static boolean validForContentPreview(Uri uri) throws SecurityException { + if (uri == null) { + return false; + } + int userId = getUserIdFromUri(uri, UserHandle.USER_CURRENT); + if (userId != UserHandle.USER_CURRENT && userId != UserHandle.myUserId()) { + Log.e(TAG, "dropped invalid content URI belonging to user " + userId); + return false; + } + return true; + } + + private static List createFilePreviewActions(ActionFactory actionFactory) { List actions = new ArrayList<>(1); //TODO(b/120417119): -- cgit v1.2.3-59-g8ed1b From 08003bb380f37a87fe22bea7bb4674e7c1f0bc30 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Tue, 7 Mar 2023 19:08:47 +0000 Subject: Copy all source intents to SelectableTargetInfo The SelectableTargetInfo subtype was originally introduced to represent results from the (now-deprecated) ChooserTargetService system. Purely for legacy reasons, we now use instances of this type to represent direct/shortcut targets (as well as a caller's custom chooser targets; OTOH the original ChooserTargetService support has been fully removed). For direct/shortcut targets, we match the target against the DisplayResolveInfo target that represented the corresponding component in our earlier intent-resolution ("all-app") results. The DisplayResolveInfo record also contains all of the resolved alternates, so we can copy them over directly to support the refinement flow. Previously, we copied over only the first (for reasons that I believe now to be obsolete). Two considerations are deliberately left out-of-scope in this CL: 1. We always use the same shortcut ID regardless of the "alternate" selected by refinement. The documented ShortcutInfo API may not really justify "migrating" this ID to some intent other than the one that was registered as a shortcut (especially, e.g., if we imagine re-querying for shortcuts based on one of the alternates; we may find that the app has explicitly registered a shortcut that *matches* the alternate, but with a different assigned ID that we could learn *only* by re-querying). For now it seems unlikely that this would have significant unintended effects, so we'll punt the hypothetical bug; otherwise I belivee we'd need either re-querying or first-order API support. 2. Refinement is still unsupported for SelectableTargetInfo targets that *aren't* joined against a corresponding DisplayResolveInfo (namely, caller custom targets; I *think* that's all?). I'm not confident whether it's important to support "alternates" for these targets, but if nothing else I imagine we'd still want to support refinement of the *primary* intent (in order to defer "heavy" computation until the "pre-fire hook"). That's presumably already broken in the current implementation/not impacted by this change, so I'll leave it for now, then follow up with a fix/new CTS test. Bug: 262805893 Test: `atest CtsSharesheetDeviceTest`. New `#testShortcutSelectionRefinedToAlternate` (pending in the same topic) exercises the fix from this CL, or fails without that fix. Change-Id: Iee4ae94c040e4c0a025068def9badb98e20f281b --- .../src/com/android/intentresolver/chooser/SelectableTargetInfo.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java index 1fbe2da7..e8847edc 100644 --- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java @@ -398,10 +398,9 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { private static List getAllSourceIntents(@Nullable DisplayResolveInfo sourceInfo) { final List results = new ArrayList<>(); if (sourceInfo != null) { - // We only queried the service for the first one in our sourceinfo. - results.add(sourceInfo.getAllSourceIntents().get(0)); + results.addAll(sourceInfo.getAllSourceIntents()); } - return results; + return results; // TODO: just use our own intent if there's no sourceInfo? } private static ComponentName getResolvedComponentName( -- cgit v1.2.3-59-g8ed1b From e7f8555d956c8bf5f338a182d8023b2740edc389 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Mon, 13 Mar 2023 12:28:41 +0000 Subject: Allow refinement of caller-provided chooser targets More specifically, this provides refinement behavior for any `SelectableTargetInfo` targets that weren't joined to a `DisplayResolveInfo` in our intent-resolution step. AFAIK this is only for the caller's `EXTRA_CHOOSER_TARGETS`. CTS coverage is provided by the pending ag/21973239 (in the same topic as this CL). Bug: 271149302 Test: `atest CtsSharesheetDeviceTest`. New `testChooserTargetsRefinement` (pending in the same topic) exercises the fix from this CL, or fails without that fix. Change-Id: Ie7772953e63fe69dfe080f75eb73ac795693b1ad --- .../intentresolver/chooser/SelectableTargetInfo.java | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java index e8847edc..74c19e67 100644 --- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java +++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java @@ -195,13 +195,13 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo); - mAllSourceIntents = getAllSourceIntents(sourceInfo); - mBaseIntentToSend = getBaseIntentToSend( baseIntentToSend, mResolvedIntent, mReferrerFillInIntent); + mAllSourceIntents = getAllSourceIntents(sourceInfo, mBaseIntentToSend); + mHashProvider = context -> { final String plaintext = getChooserTargetComponentName().getPackageName() @@ -395,12 +395,24 @@ public final class SelectableTargetInfo extends ChooserTargetInfo { return sb.toString(); } - private static List getAllSourceIntents(@Nullable DisplayResolveInfo sourceInfo) { + private static List getAllSourceIntents( + @Nullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent) { final List results = new ArrayList<>(); if (sourceInfo != null) { results.addAll(sourceInfo.getAllSourceIntents()); + } else { + // This target wasn't joined to a `DisplayResolveInfo` result from our intent-resolution + // step, so it was provided directly by the caller. We don't support alternate intents + // in this case, but we still permit refinement of the intent we'll dispatch; e.g., + // clients may use this hook to defer the computation of "lazy" extras in their share + // payload. Note this accommodation isn't strictly "necessary" because clients could + // always implement equivalent behavior by pointing custom targets back at their own app + // for any amount of further refinement/modification outside of the Sharesheet flow; + // nevertheless, it's offered as a convenience for clients who may expect their normal + // refinement logic to apply equally in the case of these "special targets." + results.add(fallbackSourceIntent); } - return results; // TODO: just use our own intent if there's no sourceInfo? + return results; } private static ComponentName getResolvedComponentName( -- cgit v1.2.3-59-g8ed1b From 7c05ee5c8b7c934776d1c74a558f29dadde5ee9d Mon Sep 17 00:00:00 2001 From: 1 Date: Thu, 9 Mar 2023 22:01:50 +0000 Subject: Switch EXTRA_MODIFY_SHARE from PendingIntent to ChooserAction Honor the label provided in the action. Bug: 272008339 Test: atest IntentResolverUnitTests Test: atest CtsSharesheetDeviceTest Change-Id: I48f9bb46f1be1f8ab42a93169b4c5ab332cd8400 --- java/res/layout/chooser_grid_preview_file.xml | 1 - java/res/layout/chooser_grid_preview_image.xml | 1 - java/res/layout/chooser_grid_preview_text.xml | 1 - java/res/values/strings.xml | 7 --- .../intentresolver/ChooserActionFactory.java | 59 ++++++++++------------ .../intentresolver/ChooserRequestParameters.java | 9 ++-- .../contentpreview/ChooserContentPreviewUi.java | 2 +- .../contentpreview/ContentPreviewUi.java | 8 +-- .../intentresolver/ChooserActionFactoryTest.kt | 12 ++++- .../UnbundledChooserActivityTest.java | 16 +++--- .../contentpreview/ChooserContentPreviewUiTest.kt | 2 +- 11 files changed, 58 insertions(+), 60 deletions(-) (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 095e5d62..6ba06b3d 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -74,7 +74,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - android:text="@string/select_files" android:gravity="center" style="@style/ReselectionAction" /> diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 792b7d4d..1c0e4c2e 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -64,7 +64,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - android:text="@string/select_images" android:gravity="center" style="@style/ReselectionAction" /> diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml index 49a2edff..f25eca9a 100644 --- a/java/res/layout/chooser_grid_preview_text.xml +++ b/java/res/layout/chooser_grid_preview_text.xml @@ -57,7 +57,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - android:text="@string/select_text" android:gravity="center" style="@style/ReselectionAction" /> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 24604ed3..6881feff 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -102,13 +102,6 @@ Use work browser - - Select Files - - Select Images - - Select Text - Exclude text diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 947155f3..1cadd314 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -97,7 +97,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio private final TargetInfo mNearbySharingTarget; private final Runnable mOnNearbyButtonClicked; private final ImmutableList mCustomActions; - private final Runnable mOnModifyShareClicked; + private final @Nullable ChooserAction mModifyShareAction; private final Consumer mExcludeSharedTextAction; private final Consumer mFinishCallback; private final ChooserActivityLogger mLogger; @@ -162,10 +162,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio logger), chooserRequest.getChooserActions(), (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) - ? createModifyShareRunnable( - chooserRequest.getModifyShareAction(), - finishCallback, - logger) + ? chooserRequest.getModifyShareAction() : null), onUpdateSharedTextIsExcluded, logger, @@ -183,7 +180,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio TargetInfo nearbySharingTarget, Runnable onNearbyButtonClicked, List customActions, - @Nullable Runnable onModifyShareClicked, + @Nullable ChooserAction modifyShareAction, Consumer onUpdateSharedTextIsExcluded, ChooserActivityLogger logger, Consumer finishCallback) { @@ -196,7 +193,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio mNearbySharingTarget = nearbySharingTarget; mOnNearbyButtonClicked = onNearbyButtonClicked; mCustomActions = ImmutableList.copyOf(customActions); - mOnModifyShareClicked = onModifyShareClicked; + mModifyShareAction = modifyShareAction; mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; mLogger = logger; mFinishCallback = finishCallback; @@ -247,8 +244,15 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio public List createCustomActions() { List actions = new ArrayList<>(); for (int i = 0; i < mCustomActions.size(); i++) { + final int position = i; ActionRow.Action actionRow = createCustomAction( - mContext, mCustomActions.get(i), mFinishCallback, i, mLogger); + mContext, + mCustomActions.get(i), + mFinishCallback, + () -> { + mLogger.logCustomActionSelected(position); + } + ); if (actionRow != null) { actions.add(actionRow); } @@ -261,27 +265,14 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio */ @Override @Nullable - public Runnable getModifyShareAction() { - return mOnModifyShareClicked; - } - - private static Runnable createModifyShareRunnable( - PendingIntent pendingIntent, - Consumer finishCallback, - ChooserActivityLogger logger) { - if (pendingIntent == null) { - return null; - } - - return () -> { - try { - pendingIntent.send(); - } catch (PendingIntent.CanceledException e) { - Log.d(TAG, "Payload reselection action has been cancelled"); - } - logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE); - finishCallback.accept(Activity.RESULT_OK); - }; + public ActionRow.Action getModifyShareAction() { + return createCustomAction( + mContext, + mModifyShareAction, + mFinishCallback, + () -> { + mLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE); + }); } /** @@ -481,8 +472,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio Context context, ChooserAction action, Consumer finishCallback, - int position, - ChooserActivityLogger logger) { + Runnable loggingRunnable) { + if (action == null || action.getAction() == null) { + return null; + } Drawable icon = action.getIcon().loadDrawable(context); if (icon == null && TextUtils.isEmpty(action.getLabel())) { return null; @@ -507,7 +500,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio } catch (PendingIntent.CanceledException e) { Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled"); } - logger.logCustomActionSelected(position); + if (loggingRunnable != null) { + loggingRunnable.run(); + } finishCallback.accept(Activity.RESULT_OK); } ); diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 3d99e475..948fe4cd 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -18,7 +18,6 @@ package com.android.intentresolver; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.PendingIntent; import android.content.ComponentName; import android.content.Intent; import android.content.IntentFilter; @@ -78,7 +77,7 @@ public class ChooserRequestParameters { private final ImmutableList mFilteredComponentNames; private final ImmutableList mCallerChooserTargets; private final @NonNull ImmutableList mChooserActions; - private final PendingIntent mModifyShareAction; + private final ChooserAction mModifyShareAction; private final boolean mRetainInOnStop; @Nullable @@ -204,7 +203,7 @@ public class ChooserRequestParameters { } @Nullable - public PendingIntent getModifyShareAction() { + public ChooserAction getModifyShareAction() { return mModifyShareAction; } @@ -353,11 +352,11 @@ public class ChooserRequestParameters { } @Nullable - private static PendingIntent getModifyShareAction(Intent intent) { + private static ChooserAction getModifyShareAction(Intent intent) { try { return intent.getParcelableExtra( Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, - PendingIntent.class); + ChooserAction.class); } catch (Throwable t) { Log.w( TAG, diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 205be444..8cc747bf 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -74,7 +74,7 @@ public final class ChooserContentPreviewUi { * Provides a share modification action, if any. */ @Nullable - Runnable getModifyShareAction(); + ActionRow.Action getModifyShareAction(); /** *

diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 39856e66..96f1c376 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -30,6 +30,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.DecelerateInterpolator; +import android.widget.TextView; import androidx.annotation.LayoutRes; @@ -117,13 +118,14 @@ abstract class ContentPreviewUi { ViewGroup layout, ChooserContentPreviewUi.ActionFactory actionFactory, FeatureFlagRepository featureFlagRepository) { - Runnable modifyShareAction = actionFactory.getModifyShareAction(); + ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction(); if (modifyShareAction != null && layout != null && featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) { - View modifyShareView = layout.findViewById(R.id.reselection_action); + TextView modifyShareView = layout.findViewById(R.id.reselection_action); if (modifyShareView != null) { + modifyShareView.setText(modifyShareAction.getLabel()); modifyShareView.setVisibility(View.VISIBLE); - modifyShareView.setOnClickListener(view -> modifyShareAction.run()); + modifyShareView.setOnClickListener(view -> modifyShareAction.getOnClicked().run()); } } } diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt index af134fcd..98c7d5ee 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -33,6 +33,7 @@ import com.android.intentresolver.flags.Flags import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat import org.junit.After +import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -50,6 +51,7 @@ class ChooserActionFactoryTest { private val logger = mock() private val flags = mock() private val actionLabel = "Action label" + private val modifyShareLabel = "Modify share" private val testAction = "com.android.intentresolver.testaction" private val countdown = CountDownLatch(1) private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() { @@ -115,7 +117,8 @@ class ChooserActionFactoryTest { fun testModifyShareAction() { val factory = createFactory(includeModifyShare = true) - factory.modifyShareAction!!.run() + val action = factory.modifyShareAction ?: error("Modify share action should not be null") + action.onClicked.run() Mockito.verify(logger).logActionSelected( eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE)) @@ -137,7 +140,12 @@ class ChooserActionFactoryTest { whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action)) if (includeModifyShare) { - whenever(chooserRequest.modifyShareAction).thenReturn(testPendingIntent) + val modifyShare = ChooserAction.Builder( + Icon.createWithResource("", Resources.ID_NULL), + modifyShareLabel, + testPendingIntent + ).build() + whenever(chooserRequest.modifyShareAction).thenReturn(modifyShare) } return ChooserActionFactory( diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 0aab0536..65e95e30 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -1726,13 +1726,17 @@ public class UnbundledChooserActivityTest { Context testContext = InstrumentationRegistry.getInstrumentation().getContext(); final String modifyShareAction = "test-broadcast-receiver-action"; Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null); + String label = "modify share"; + PendingIntent pendingIntent = PendingIntent.getBroadcast( + testContext, + 123, + new Intent(modifyShareAction), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT); + ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap( + createBitmap()), label, pendingIntent).build(); chooserIntent.putExtra( Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION, - PendingIntent.getBroadcast( - testContext, - 123, - new Intent(modifyShareAction), - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT)); + action); // Start activity mActivityRule.launchActivity(chooserIntent); waitForIdle(); @@ -1747,7 +1751,7 @@ public class UnbundledChooserActivityTest { testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction)); try { - onView(withText(R.string.select_text)).perform(click()); + onView(withText(label)).perform(click()); broadcastInvoked.await(); } finally { testContext.unregisterReceiver(testReceiver); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index d870a8c2..23bfaf9f 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -54,7 +54,7 @@ class ChooserContentPreviewUiTest { override fun createEditButton(): ActionRow.Action? = null override fun createNearbyButton(): ActionRow.Action? = null override fun createCustomActions(): List = emptyList() - override fun getModifyShareAction(): Runnable? = null + override fun getModifyShareAction(): ActionRow.Action? = null override fun getExcludeSharedTextAction(): Consumer = Consumer {} } private val transitionCallback = mock() -- cgit v1.2.3-59-g8ed1b From 961d141bc72743ee8c42ce21bb6b0e5e01b30c4c Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Mon, 13 Mar 2023 19:57:27 +0000 Subject: Stop sharing framework strings from unbundled Java This kind of sharing was a relic of unbundling, and is known to cause inscrutable failures (e.g. b/270760957) when the unbundled tests were run on a device with an out-of-date framework version. We've already hard-forked (duplicated into the IntentResolver package) most string resources, and this CL addresses the remainder, w/ exceptions: 1. Layout XML and other resource files may not have the same compatibility issues, so I've left them unchanged or now; we may need to make a similar change to those files in the future. 2. I left one string, `com.android.internal.R.string.copy`, because I don't think we'd ever want to diverge from the system value and it's currently exported in the frameworks `public-final.xml` resource (so there's no risk of binary-incompatibility issues). In some cases, the resource-sharing wasn't apparent due to file-level imports of `com.android.internal.R`; I removed those imports (so that all the unbundled code loads resources from the `intentresolver` package by default) and converted the non-string resource references to explicitly reference the framework symbols by fully-qualified name. This only addresses string resources and may be inadequate to prevent all regressions in the class of b/270760957, and view ID resources are particularly concerning because it wouldn't be straightforward to fix them by an analogous "hard-fork" process. In fact, an earlier workaround (removed in ag/20065287) had separate cases specifically to address resource sharing for strings vs. view IDs, so we may expect to need some fix there. If we encounter similar regressions based on the view IDs (and don't have any better ideas), we may be able to reinstate the workaround from ag/20065287 (just for the view IDs; strings should never need the old workaround now). Bug: 270760957 Test: `atest IntentResolverUnitTests`, before and after adding a new placeholder string resource at the top of frameworks' `strings.xml`. Prior to this CL, that modification would've caused the tests to start failing unless the framework was rebuilt (e.g. by `mp droid`). Change-Id: Ifaf069124ba677a79517894d7aba847c5d869b74 --- java/res/values/strings.xml | 86 +++++++++++++++++++++- .../intentresolver/ChooserActionFactory.java | 2 +- .../intentresolver/ChooserRequestParameters.java | 3 +- .../intentresolver/IntentForwarderActivity.java | 4 +- .../NoAppsAvailableEmptyStateProvider.java | 1 - .../android/intentresolver/ResolverActivity.java | 66 ++++++++--------- .../WorkProfilePausedEmptyStateProvider.java | 1 - .../intentresolver/ResolverActivityTest.java | 57 +++++++------- .../UnbundledChooserActivityTest.java | 4 +- .../UnbundledChooserActivityWorkProfileTest.java | 3 +- 10 files changed, 151 insertions(+), 76 deletions(-) (limited to 'java/src') diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 24604ed3..1cd39d42 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -17,17 +17,92 @@ - IntentResolver - + IntentResolver + + + Complete action using + + Complete action using %1$s + + Complete action + + Open with + + Open with %1$s Open + + Open %1$s links with + + Open links with + + Open links with %1$s + + Open %1$s links with %2$s + + Give access + + Edit with + + Edit with %1$s + + Edit + + Share + + Share with %1$s + + Share + + Send using + + Send using %1$s + + Send + + Select a Home app + + Use %1$s as Home + + Capture image + + + Capture image with + + Capture image with %1$s + + Capture image + Use a different app + + Choose an action No apps can perform this action. + + You\'re using this app outside of your work profile + + You\'re using this app in your work profile + Always @@ -36,12 +111,19 @@ from the activity resolver to use just this once. [CHAR LIMIT=25] --> Just once + + %1$s doesn\'t support work profile + Pin %1$s Unpin %1$s + + Edit + {count, plural, =1 {{file_name} + # file} other {{file_name} + # files} diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 947155f3..82103b39 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -386,7 +386,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( originalIntent, ri, - context.getString(com.android.internal.R.string.screenshot_edit), + context.getString(R.string.screenshot_edit), "", resolveIntent, null); diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 3d99e475..dbd72a1f 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -310,8 +310,7 @@ public class ChooserRequestParameters { requestedTitle = null; } - int defaultTitleRes = - (requestedTitle == null) ? com.android.internal.R.string.chooseActivity : 0; + int defaultTitleRes = (requestedTitle == null) ? R.string.chooseActivity : 0; return Pair.create(requestedTitle, defaultTitleRes); } diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java index 78240250..5e8945f1 100644 --- a/java/src/com/android/intentresolver/IntentForwarderActivity.java +++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java @@ -162,13 +162,13 @@ public class IntentForwarderActivity extends Activity { private String getForwardToPersonalMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_PERSONAL, - () -> getString(com.android.internal.R.string.forward_intent_to_owner)); + () -> getString(R.string.forward_intent_to_owner)); } private String getForwardToWorkMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_WORK, - () -> getString(com.android.internal.R.string.forward_intent_to_work)); + () -> getString(R.string.forward_intent_to_work)); } private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) { diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java index c1373f4b..d424f295 100644 --- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java @@ -31,7 +31,6 @@ import android.stats.devicepolicy.nano.DevicePolicyEnums; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; -import com.android.internal.R; import java.util.List; diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index d224299e..a240968b 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -237,47 +237,43 @@ public class ResolverActivity extends FragmentActivity implements private enum ActionTitle { VIEW(Intent.ACTION_VIEW, - com.android.internal.R.string.whichViewApplication, - com.android.internal.R.string.whichViewApplicationNamed, - com.android.internal.R.string.whichViewApplicationLabel), + R.string.whichViewApplication, + R.string.whichViewApplicationNamed, + R.string.whichViewApplicationLabel), EDIT(Intent.ACTION_EDIT, - com.android.internal.R.string.whichEditApplication, - com.android.internal.R.string.whichEditApplicationNamed, - com.android.internal.R.string.whichEditApplicationLabel), + R.string.whichEditApplication, + R.string.whichEditApplicationNamed, + R.string.whichEditApplicationLabel), SEND(Intent.ACTION_SEND, - com.android.internal.R.string.whichSendApplication, - com.android.internal.R.string.whichSendApplicationNamed, - com.android.internal.R.string.whichSendApplicationLabel), + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), SENDTO(Intent.ACTION_SENDTO, - com.android.internal.R.string.whichSendToApplication, - com.android.internal.R.string.whichSendToApplicationNamed, - com.android.internal.R.string.whichSendToApplicationLabel), + R.string.whichSendToApplication, + R.string.whichSendToApplicationNamed, + R.string.whichSendToApplicationLabel), SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, - com.android.internal.R.string.whichSendApplication, - com.android.internal.R.string.whichSendApplicationNamed, - com.android.internal.R.string.whichSendApplicationLabel), + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, - com.android.internal.R.string.whichImageCaptureApplication, - com.android.internal.R.string.whichImageCaptureApplicationNamed, - com.android.internal.R.string.whichImageCaptureApplicationLabel), + R.string.whichImageCaptureApplication, + R.string.whichImageCaptureApplicationNamed, + R.string.whichImageCaptureApplicationLabel), DEFAULT(null, - com.android.internal.R.string.whichApplication, - com.android.internal.R.string.whichApplicationNamed, - com.android.internal.R.string.whichApplicationLabel), + R.string.whichApplication, + R.string.whichApplicationNamed, + R.string.whichApplicationLabel), HOME(Intent.ACTION_MAIN, - com.android.internal.R.string.whichHomeApplication, - com.android.internal.R.string.whichHomeApplicationNamed, - com.android.internal.R.string.whichHomeApplicationLabel); + R.string.whichHomeApplication, + R.string.whichHomeApplicationNamed, + R.string.whichHomeApplicationLabel); // titles for layout that deals with http(s) intents - public static final int BROWSABLE_TITLE_RES = - com.android.internal.R.string.whichOpenLinksWith; - public static final int BROWSABLE_HOST_TITLE_RES = - com.android.internal.R.string.whichOpenHostLinksWith; - public static final int BROWSABLE_HOST_APP_TITLE_RES = - com.android.internal.R.string.whichOpenHostLinksWithApp; - public static final int BROWSABLE_APP_TITLE_RES = - com.android.internal.R.string.whichOpenLinksWithApp; + public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; + public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; + public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; + public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; public final String action; public final int titleRes; @@ -1361,13 +1357,13 @@ public class ResolverActivity extends FragmentActivity implements private String getForwardToPersonalMsg() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_PERSONAL, - () -> getString(com.android.internal.R.string.forward_intent_to_owner)); + () -> getString(R.string.forward_intent_to_owner)); } private String getForwardToWorkMsg() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_WORK, - () -> getString(com.android.internal.R.string.forward_intent_to_work)); + () -> getString(R.string.forward_intent_to_work)); } /** @@ -1544,7 +1540,7 @@ public class ResolverActivity extends FragmentActivity implements return getSystemService(DevicePolicyManager.class).getResources().getString( RESOLVER_WORK_PROFILE_NOT_SUPPORTED, () -> getString( - com.android.internal.R.string.activity_resolver_work_profiles_support, + R.string.activity_resolver_work_profiles_support, launcherName), launcherName); } diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java index 0333039b..2f3dfbd5 100644 --- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java @@ -29,7 +29,6 @@ import android.stats.devicepolicy.nano.DevicePolicyEnums; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener; -import com.android.internal.R; /** * Chooser/ResolverActivity empty state provider that returns empty state which is shown when diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index ae1b99f8..e2772423 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -55,7 +55,6 @@ import androidx.test.rule.ActivityTestRule; import androidx.test.runner.AndroidJUnit4; import com.android.intentresolver.widget.ResolverDrawerLayout; -import com.android.internal.R; import org.junit.Before; import org.junit.Ignore; @@ -117,7 +116,7 @@ public class ResolverActivityTest { ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); onView(withText(toChoose.activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); @@ -133,13 +132,13 @@ public class ResolverActivityTest { waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - final View viewPager = activity.findViewById(R.id.profile_pager); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); final int initialResolverHeight = viewPager.getHeight(); activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight = initialResolverHeight - 1; // Force a relayout @@ -153,7 +152,7 @@ public class ResolverActivityTest { activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight = initialResolverHeight + 1; // Force a relayout @@ -175,10 +174,11 @@ public class ResolverActivityTest { waitForIdle(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); - final View viewPager = activity.findViewById(R.id.profile_pager); - final View divider = activity.findViewById(R.id.divider); + final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager); + final View divider = activity.findViewById(com.android.internal.R.id.divider); final RelativeLayout profileView = - (RelativeLayout) activity.findViewById(R.id.profile_button).getParent(); + (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button) + .getParent(); assertThat("Drawer should show at bottom by default", profileView.getBottom() + divider.getHeight() == viewPager.getTop() && profileView.getTop() > 0); @@ -186,7 +186,7 @@ public class ResolverActivityTest { activity.runOnUiThread(() -> { ResolverDrawerLayout layout = (ResolverDrawerLayout) activity.findViewById( - R.id.contentPanel); + com.android.internal.R.id.contentPanel); layout.setShowAtTop(true); }); waitForIdle(); @@ -218,7 +218,7 @@ public class ResolverActivityTest { return true; }; - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -251,7 +251,7 @@ public class ResolverActivityTest { // We pick the first one as there is another one in the work profile side onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); @@ -280,7 +280,7 @@ public class ResolverActivityTest { }; // Confirm that the button bar is disabled by default - onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); // Make a stable copy of the components as the original list may be modified List stableCopy = @@ -288,7 +288,7 @@ public class ResolverActivityTest { onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -321,7 +321,7 @@ public class ResolverActivityTest { }; // Confirm that the button bar is disabled by default - onView(withId(R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); // Make a stable copy of the components as the original list may be modified List stableCopy = @@ -329,7 +329,7 @@ public class ResolverActivityTest { onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); - onView(withId(R.id.button_once)).perform(click()); + onView(withId(com.android.internal.R.id.button_once)).perform(click()); waitForIdle(); assertThat(chosen[0], is(toChoose)); } @@ -342,7 +342,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.tabs)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed())); } @Test @@ -352,7 +352,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.tabs)).check(matches(not(isDisplayed()))); + onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed()))); } @Test @@ -447,7 +447,7 @@ public class ResolverActivityTest { onView(first(allOf(withText(workResolvedComponentInfos.get(0) .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); @@ -484,7 +484,7 @@ public class ResolverActivityTest { final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); - TextView headerText = activity.findViewById(R.id.title); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); String initialText = headerText.getText().toString(); assertFalse(initialText.isEmpty(), "Header text is empty."); assertThat(headerText.getVisibility(), is(View.VISIBLE)); @@ -501,7 +501,7 @@ public class ResolverActivityTest { final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); - TextView headerText = activity.findViewById(R.id.title); + TextView headerText = activity.findViewById(com.android.internal.R.id.title); String initialText = headerText.getText().toString(); onView(withText(R.string.resolver_work_tab)) .perform(click()); @@ -539,7 +539,7 @@ public class ResolverActivityTest { .getResolveInfoAt(0).activityInfo.applicationInfo.name), isDisplayed()))) .perform(click()); - onView(withId(R.id.button_once)) + onView(withId(com.android.internal.R.id.button_once)) .perform(click()); waitForIdle(); @@ -563,7 +563,7 @@ public class ResolverActivityTest { waitForIdle(); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_cross_profile_blocked)) @@ -585,7 +585,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -607,7 +607,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -631,7 +631,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); @@ -655,7 +655,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed())); } @Test @@ -678,7 +678,8 @@ public class ResolverActivityTest { private void assertNotMiniResolver() { try { - onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed())); + onView(withId(com.android.internal.R.id.open_cross_profile)) + .check(matches(isDisplayed())); } catch (NoMatchingViewException e) { return; } @@ -699,7 +700,7 @@ public class ResolverActivityTest { mActivityRule.launchActivity(sendIntent); waitForIdle(); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); onView(withText(R.string.resolver_work_tab)).perform(click()); waitForIdle(); diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 0aab0536..3bf9f1d8 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -350,7 +350,7 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test")); waitForIdle(); onView(withId(android.R.id.title)) - .check(matches(withText(com.android.internal.R.string.whichSendApplication))); + .check(matches(withText(R.string.whichSendApplication))); } @Test @@ -362,7 +362,7 @@ public class UnbundledChooserActivityTest { mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); onView(withId(android.R.id.title)) - .check(matches(withText(com.android.internal.R.string.whichSendApplication))); + .check(matches(withText(R.string.whichSendApplication))); } @Test diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java index 87dc1b9d..6c1edfbc 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -49,7 +49,6 @@ import androidx.test.espresso.NoMatchingViewException; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab; -import com.android.internal.R; import junit.framework.AssertionFailedError; @@ -356,7 +355,7 @@ public class UnbundledChooserActivityWorkProfileTest { } }); - onView(withId(R.id.contentPanel)) + onView(withId(com.android.internal.R.id.contentPanel)) .perform(swipeUp()); waitForIdle(); } -- cgit v1.2.3-59-g8ed1b From 4649ef1769d53727d59423f184cb3ee068ce40db Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 13 Mar 2023 16:39:23 -0700 Subject: Add unified preview UI ChooserContentPreviewUi applies various heuristic to determine if each shared URI has a preview and, if any, displays a scrollable preview list. Each preview item in the list is badge accroding ot its type: no badge for images, a video-file badge for videos and a generic-file badge for all others. All URIs without a previwe are groupped under a single item at list end (+N files). Collateral changes: * FileContentPreviewUi$FileInfo is moved into the package level; * ChooserContentPreviewUi$ImageMimeTypeClassifier internface is moved into the package level, renamted to MimeTypeClassifier and defines a new method, isVideoType(); * ScrollableImagePreviewView is modified to support badges and the "+N" item; * A new class, UnfifiedContentPreviewUi is clonned from ImageContentPreviewUi class, and is reponsible for drawing the new unified preview ui, ImageContentPreviewUi is used only with the legacy image content preview. Bug: 271613784 Test: manual testing Change-Id: Ia25f5a1565226ac679cc8ecefd58acb95cb60142 --- java/res/drawable/content_preview_badge_bg.xml | 27 ++ java/res/drawable/ic_file_video.xml | 27 ++ java/res/layout/image_preview_image_item.xml | 36 ++- java/res/layout/image_preview_other_item.xml | 31 +++ java/res/values/attrs.xml | 4 + java/res/values/dimens.xml | 1 + java/res/values/strings.xml | 7 + .../android/intentresolver/ChooserActivity.java | 2 +- .../intentresolver/ImagePreviewImageLoader.kt | 10 +- .../contentpreview/ChooserContentPreviewUi.java | 306 +++++++++++++-------- .../contentpreview/FileContentPreviewUi.java | 101 +------ .../intentresolver/contentpreview/FileInfo.kt | 48 ++++ .../contentpreview/ImageContentPreviewUi.java | 19 +- .../contentpreview/MimeTypeClassifier.java | 38 +++ .../contentpreview/UnifiedContentPreviewUi.java | 202 ++++++++++++++ .../widget/ChooserImagePreviewView.kt | 2 +- .../intentresolver/widget/ImagePreviewView.kt | 1 - .../widget/RoundedRectImageView.java | 13 +- .../widget/ScrollableImagePreviewView.kt | 125 ++++++--- .../contentpreview/ChooserContentPreviewUiTest.kt | 91 +++++- 20 files changed, 826 insertions(+), 265 deletions(-) create mode 100644 java/res/drawable/content_preview_badge_bg.xml create mode 100644 java/res/drawable/ic_file_video.xml create mode 100644 java/res/layout/image_preview_other_item.xml create mode 100644 java/src/com/android/intentresolver/contentpreview/FileInfo.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java create mode 100644 java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java (limited to 'java/src') diff --git a/java/res/drawable/content_preview_badge_bg.xml b/java/res/drawable/content_preview_badge_bg.xml new file mode 100644 index 00000000..087247e7 --- /dev/null +++ b/java/res/drawable/content_preview_badge_bg.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/java/res/drawable/ic_file_video.xml b/java/res/drawable/ic_file_video.xml new file mode 100644 index 00000000..8c7a3650 --- /dev/null +++ b/java/res/drawable/ic_file_video.xml @@ -0,0 +1,27 @@ + + + + + + diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml index c18cc279..81fa5c8e 100644 --- a/java/res/layout/image_preview_image_item.xml +++ b/java/res/layout/image_preview_image_item.xml @@ -14,11 +14,35 @@ ~ limitations under the License. --> - + android:layout_height="@dimen/chooser_preview_image_height" > + + + + + + + + diff --git a/java/res/layout/image_preview_other_item.xml b/java/res/layout/image_preview_other_item.xml new file mode 100644 index 00000000..b7cc4350 --- /dev/null +++ b/java/res/layout/image_preview_other_item.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml index 2f2bbda2..eba6b9b7 100644 --- a/java/res/values/attrs.xml +++ b/java/res/values/attrs.xml @@ -41,4 +41,8 @@ + + + + diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index 87eec7fb..af90c4ef 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -19,6 +19,7 @@ 412dp 28dp + 14dp 25dp 18dp 16dp diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 24604ed3..11f8bc59 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -48,6 +48,13 @@ } + + {count, plural, + =1 {+ # file} + other {+ # files} + } + + No recommended people to share with diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index ae5be26d..37a17e79 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -714,7 +714,7 @@ public class ChooserActivity extends ResolverActivity implements } @VisibleForTesting - protected boolean isImageType(String mimeType) { + protected boolean isImageType(@Nullable String mimeType) { return mimeType != null && mimeType.startsWith("image/"); } diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt index 7b6651a2..9650403e 100644 --- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt @@ -19,6 +19,7 @@ package com.android.intentresolver import android.content.Context import android.graphics.Bitmap import android.net.Uri +import android.util.Log import android.util.Size import androidx.annotation.GuardedBy import androidx.annotation.VisibleForTesting @@ -32,6 +33,8 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import java.util.function.Consumer +private const val TAG = "ImagePreviewImageLoader" + @VisibleForTesting class ImagePreviewImageLoader @JvmOverloads constructor( private val context: Context, @@ -79,9 +82,12 @@ class ImagePreviewImageLoader @JvmOverloads constructor( } private fun CompletableDeferred.loadBitmap(uri: Uri) { - val bitmap = runCatching { + val bitmap = try { context.contentResolver.loadThumbnail(uri, thumbnailSize, null) - }.getOrNull() + } catch (t: Throwable) { + Log.d(TAG, "failed to load $uri preview", t) + null + } complete(bitmap) } } diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 205be444..526b15ae 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -16,17 +16,23 @@ package com.android.intentresolver.contentpreview; -import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE; +import static android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL; + import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; -import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT; import android.content.ClipData; import android.content.ClipDescription; import android.content.ContentInterface; import android.content.Intent; import android.content.res.Resources; +import android.database.Cursor; +import android.media.MediaMetadata; import android.net.Uri; import android.os.RemoteException; +import android.provider.DocumentsContract; +import android.provider.Downloads; +import android.provider.OpenableColumns; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -34,14 +40,14 @@ import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; +import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; -import com.android.intentresolver.widget.ImagePreviewView; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.function.Consumer; -import java.util.stream.Collectors; /** * Collection of helpers for building the content preview UI displayed in @@ -88,24 +94,12 @@ public final class ChooserContentPreviewUi { Consumer getExcludeSharedTextAction(); } - /** - * Testing shim to specify whether a given mime type is considered to be an "image." - * - * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests, - * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this - * class. - */ - public interface ImageMimeTypeClassifier { - /** @return whether the specified {@code mimeType} is classified as an "image" type. */ - boolean isImageType(String mimeType); - } - private final ContentPreviewUi mContentPreviewUi; public ChooserContentPreviewUi( Intent targetIntent, ContentInterface contentResolver, - ImageMimeTypeClassifier imageClassifier, + MimeTypeClassifier imageClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, @@ -127,37 +121,78 @@ public final class ChooserContentPreviewUi { private ContentPreviewUi createContentPreview( Intent targetIntent, ContentInterface contentResolver, - ImageMimeTypeClassifier imageClassifier, + MimeTypeClassifier typeClassifier, ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, FeatureFlagRepository featureFlagRepository) { - int type = findPreferredContentPreview(targetIntent, contentResolver, imageClassifier); - switch (type) { - case CONTENT_PREVIEW_TEXT: - return createTextPreview( - targetIntent, actionFactory, imageLoader, featureFlagRepository); - case CONTENT_PREVIEW_FILE: - return new FileContentPreviewUi( - extractContentUris(targetIntent), - actionFactory, - imageLoader, - contentResolver, - featureFlagRepository); + /* In {@link android.content.Intent#getType}, the app may specify a very general mime type + * that broadly covers all data being shared, such as {@literal *}/* when sending an image + * and text. We therefore should inspect each item for the preferred type, in order: IMAGE, + * FILE, TEXT. */ + final String action = targetIntent.getAction(); + final String type = targetIntent.getType(); + final boolean isSend = Intent.ACTION_SEND.equals(action); + final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action); - case CONTENT_PREVIEW_IMAGE: - return createImagePreview( - targetIntent, + if (!(isSend || isSendMultiple) + || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) { + return createTextPreview( + targetIntent, actionFactory, imageLoader, featureFlagRepository); + } + List uris = extractContentUris(targetIntent); + if (uris.isEmpty()) { + return createTextPreview( + targetIntent, actionFactory, imageLoader, featureFlagRepository); + } + ArrayList files = new ArrayList<>(uris.size()); + int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); + if (previewCount == 0) { + return new FileContentPreviewUi( + files, + actionFactory, + imageLoader, + featureFlagRepository); + } + if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { + return new UnifiedContentPreviewUi( + files, + targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), + actionFactory, + imageLoader, + typeClassifier, + transitionElementStatusCallback, + featureFlagRepository); + } + if (previewCount < uris.size()) { + return new FileContentPreviewUi( + files, + actionFactory, + imageLoader, + featureFlagRepository); + } + // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up + // here. To preserve the legacy behavior, before using it, check that all uris are images. + for (FileInfo fileInfo: files) { + if (!typeClassifier.isImageType(fileInfo.getMimeType())) { + return new FileContentPreviewUi( + files, actionFactory, - contentResolver, - imageClassifier, imageLoader, - transitionElementStatusCallback, featureFlagRepository); + } } - - return new NoContextPreviewUi(type); + return new ImageContentPreviewUi( + files.stream() + .map(FileInfo::getPreviewUri) + .filter(Objects::nonNull) + .toList(), + targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), + actionFactory, + imageLoader, + transitionElementStatusCallback, + featureFlagRepository); } public int getPreferredContentPreview() { @@ -174,61 +209,98 @@ public final class ChooserContentPreviewUi { return mContentPreviewUi.display(resources, layoutInflater, parent); } - /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */ - @ContentPreviewType - private static int findPreferredContentPreview( - Intent targetIntent, - ContentInterface resolver, - ImageMimeTypeClassifier imageClassifier) { - /* In {@link android.content.Intent#getType}, the app may specify a very general mime type - * that broadly covers all data being shared, such as {@literal *}/* when sending an image - * and text. We therefore should inspect each item for the preferred type, in order: IMAGE, - * FILE, TEXT. */ - final String action = targetIntent.getAction(); - final String type = targetIntent.getType(); - final boolean isSend = Intent.ACTION_SEND.equals(action); - final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action); - - if (!(isSend || isSendMultiple) - || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) { - return CONTENT_PREVIEW_TEXT; + private static int readFileInfo( + ContentInterface contentResolver, + MimeTypeClassifier typeClassifier, + List uris, + List fileInfos) { + int previewCount = 0; + for (Uri uri: uris) { + FileInfo fileInfo = getFileInfo(contentResolver, typeClassifier, uri); + if (fileInfo.getPreviewUri() != null) { + previewCount++; + } + fileInfos.add(fileInfo); } + return previewCount; + } - if (isSend) { - Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM); - return findPreferredContentPreview(uri, resolver, imageClassifier); + private static FileInfo getFileInfo( + ContentInterface resolver, MimeTypeClassifier typeClassifier, Uri uri) { + FileInfo.Builder builder = new FileInfo.Builder(uri) + .withName(getFileName(uri)); + String mimeType = getType(resolver, uri); + builder.withMimeType(mimeType); + if (typeClassifier.isImageType(mimeType)) { + return builder.withPreviewUri(uri).build(); } - - List uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); - if (uris == null || uris.isEmpty()) { - return CONTENT_PREVIEW_TEXT; + readFileMetadata(resolver, uri, builder); + if (builder.getPreviewUri() == null) { + readOtherFileTypes(resolver, uri, typeClassifier, builder); } + return builder.build(); + } - for (Uri uri : uris) { - // Defaulting to file preview when there are mixed image/file types is - // preferable, as it shows the user the correct number of items being shared - int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier); - if (uriPreviewType == CONTENT_PREVIEW_FILE) { - return CONTENT_PREVIEW_FILE; + private static void readFileMetadata( + ContentInterface resolver, Uri uri, FileInfo.Builder builder) { + Cursor cursor = query(resolver, uri); + if (cursor == null || !cursor.moveToFirst()) { + return; + } + int flagColIdx = -1; + int displayIconUriColIdx = -1; + int nameColIndex = -1; + int titleColIndex = -1; + String[] columns = cursor.getColumnNames(); + // TODO: double-check why Cursor#getColumnInded didn't work + for (int i = 0; i < columns.length; i++) { + String columnName = columns[i]; + if (DocumentsContract.Document.COLUMN_FLAGS.equals(columnName)) { + flagColIdx = i; + } else if (MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI.equals(columnName)) { + displayIconUriColIdx = i; + } else if (OpenableColumns.DISPLAY_NAME.equals(columnName)) { + nameColIndex = i; + } else if (Downloads.Impl.COLUMN_TITLE.equals(columnName)) { + titleColIndex = i; } } + String fileName = ""; + if (nameColIndex >= 0) { + fileName = cursor.getString(nameColIndex); + } else if (titleColIndex >= 0) { + fileName = cursor.getString(titleColIndex); + } + if (!TextUtils.isEmpty(fileName)) { + builder.withName(fileName); + } - return CONTENT_PREVIEW_IMAGE; - } - - @ContentPreviewType - private static int findPreferredContentPreview( - Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier) { - if (uri == null) { - return CONTENT_PREVIEW_TEXT; + Uri previewUri = null; + if (flagColIdx >= 0 && ((cursor.getInt(flagColIdx) & FLAG_SUPPORTS_THUMBNAIL) != 0)) { + previewUri = uri; + } else if (displayIconUriColIdx >= 0) { + String uriStr = cursor.getString(displayIconUriColIdx); + previewUri = uriStr == null ? null : Uri.parse(uriStr); } + if (previewUri != null) { + builder.withPreviewUri(previewUri); + } + } - String mimeType = null; - try { - mimeType = resolver.getType(uri); - } catch (RemoteException ignored) { + private static void readOtherFileTypes( + ContentInterface resolver, + Uri uri, + MimeTypeClassifier typeClassifier, + FileInfo.Builder builder) { + String[] otherTypes = getStreamTypes(resolver, uri); + if (otherTypes != null && otherTypes.length > 0) { + for (String mimeType : otherTypes) { + if (typeClassifier.isImageType(mimeType)) { + builder.withPreviewUri(uri); + break; + } + } } - return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; } private static TextContentPreviewUi createTextPreview( @@ -255,39 +327,6 @@ public final class ChooserContentPreviewUi { featureFlagRepository); } - static ImageContentPreviewUi createImagePreview( - Intent targetIntent, - ChooserContentPreviewUi.ActionFactory actionFactory, - ContentInterface contentResolver, - ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier, - ImageLoader imageLoader, - ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { - CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); - String action = targetIntent.getAction(); - // TODO: why don't we use image classifier for single-element ACTION_SEND? - final List imageUris = Intent.ACTION_SEND.equals(action) - ? extractContentUris(targetIntent) - : extractContentUris(targetIntent) - .stream() - .filter(uri -> { - String type = null; - try { - type = contentResolver.getType(uri); - } catch (RemoteException ignored) { - } - return imageClassifier.isImageType(type); - }) - .collect(Collectors.toList()); - return new ImageContentPreviewUi( - imageUris, - text, - actionFactory, - imageLoader, - transitionElementStatusCallback, - featureFlagRepository); - } - private static List extractContentUris(Intent targetIntent) { List uris = new ArrayList<>(); if (Intent.ACTION_SEND.equals(targetIntent.getAction())) { @@ -307,4 +346,41 @@ public final class ChooserContentPreviewUi { } return uris; } + + @Nullable + private static String getType(ContentInterface resolver, Uri uri) { + try { + return resolver.getType(uri); + } catch (RemoteException e) { + return null; + } + } + + @Nullable + private static Cursor query(ContentInterface resolver, Uri uri) { + try { + return resolver.query(uri, null, null, null); + } catch (RemoteException e) { + return null; + } + } + + @Nullable + private static String[] getStreamTypes(ContentInterface resolver, Uri uri) { + try { + return resolver.getStreamTypes(uri, "*/*"); + } catch (RemoteException e) { + return null; + } + } + + private static String getFileName(Uri uri) { + String fileName = uri.getPath(); + fileName = fileName == null ? "" : fileName; + int index = fileName.lastIndexOf('/'); + if (index != -1) { + fileName = fileName.substring(index + 1); + } + return fileName; + } } diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 7cd71475..2c5def8b 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -16,15 +16,7 @@ package com.android.intentresolver.contentpreview; -import android.content.ContentInterface; import android.content.res.Resources; -import android.database.Cursor; -import android.net.Uri; -import android.os.RemoteException; -import android.provider.DocumentsContract; -import android.provider.Downloads; -import android.provider.OpenableColumns; -import android.text.TextUtils; import android.util.Log; import android.util.PluralsMessageFormatter; import android.view.LayoutInflater; @@ -49,21 +41,19 @@ class FileContentPreviewUi extends ContentPreviewUi { private static final String PLURALS_COUNT = "count"; private static final String PLURALS_FILE_NAME = "file_name"; - private final List mUris; + private final List mFiles; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; - private final ContentInterface mContentResolver; private final FeatureFlagRepository mFeatureFlagRepository; - FileContentPreviewUi(List uris, + FileContentPreviewUi( + List files, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - ContentInterface contentResolver, FeatureFlagRepository featureFlagRepository) { - mUris = uris; + mFiles = files; mActionFactory = actionFactory; mImageLoader = imageLoader; - mContentResolver = contentResolver; mFeatureFlagRepository = featureFlagRepository; } @@ -85,7 +75,7 @@ class FileContentPreviewUi extends ContentPreviewUi { ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_file, parent, false); - final int uriCount = mUris.size(); + final int uriCount = mFiles.size(); if (uriCount == 0) { contentPreviewLayout.setVisibility(View.GONE); @@ -95,13 +85,13 @@ class FileContentPreviewUi extends ContentPreviewUi { } if (uriCount == 1) { - loadFileUriIntoView(mUris.get(0), contentPreviewLayout, mImageLoader, mContentResolver); + loadFileUriIntoView(mFiles.get(0), contentPreviewLayout, mImageLoader); } else { - FileInfo fileInfo = extractFileInfo(mUris.get(0), mContentResolver); + FileInfo fileInfo = mFiles.get(0); int remUriCount = uriCount - 1; Map arguments = new HashMap<>(); arguments.put(PLURALS_COUNT, remUriCount); - arguments.put(PLURALS_FILE_NAME, fileInfo.name); + arguments.put(PLURALS_FILE_NAME, fileInfo.getName()); String fileName = PluralsMessageFormatter.format(resources, arguments, R.string.file_count); @@ -143,19 +133,16 @@ class FileContentPreviewUi extends ContentPreviewUi { } private static void loadFileUriIntoView( - final Uri uri, + final FileInfo fileInfo, final View parent, - final ImageLoader imageLoader, - final ContentInterface contentResolver) { - FileInfo fileInfo = extractFileInfo(uri, contentResolver); - + final ImageLoader imageLoader) { TextView fileNameView = parent.findViewById( com.android.internal.R.id.content_preview_filename); - fileNameView.setText(fileInfo.name); + fileNameView.setText(fileInfo.getName()); - if (fileInfo.hasThumbnail) { + if (fileInfo.getPreviewUri() != null) { imageLoader.loadImage( - uri, + fileInfo.getPreviewUri(), (bitmap) -> updateViewWithImage( parent.findViewById( com.android.internal.R.id.content_preview_file_thumbnail), @@ -171,66 +158,4 @@ class FileContentPreviewUi extends ContentPreviewUi { fileIconView.setImageResource(R.drawable.chooser_file_generic); } } - - private static FileInfo extractFileInfo(Uri uri, ContentInterface resolver) { - String fileName = null; - boolean hasThumbnail = false; - - try (Cursor cursor = queryResolver(resolver, uri)) { - if (cursor != null && cursor.getCount() > 0) { - int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); - int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE); - int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS); - - cursor.moveToFirst(); - if (nameIndex != -1) { - fileName = cursor.getString(nameIndex); - } else if (titleIndex != -1) { - fileName = cursor.getString(titleIndex); - } - - if (flagsIndex != -1) { - hasThumbnail = (cursor.getInt(flagsIndex) - & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0; - } - } - } catch (SecurityException | NullPointerException e) { - // The ContentResolver already logs the exception. Log something more informative. - Log.w( - TAG, - "Could not load (" + uri.toString() + ") thumbnail/name for preview. If " - + "desired, consider using Intent#createChooser to launch the ChooserActivity, " - + "and set your Intent's clipData and flags in accordance with that method's " - + "documentation"); - } - - if (TextUtils.isEmpty(fileName)) { - fileName = uri.getPath(); - fileName = fileName == null ? "" : fileName; - int index = fileName.lastIndexOf('/'); - if (index != -1) { - fileName = fileName.substring(index + 1); - } - } - - return new FileInfo(fileName, hasThumbnail); - } - - private static Cursor queryResolver(ContentInterface resolver, Uri uri) { - try { - return resolver.query(uri, null, null, null); - } catch (RemoteException e) { - return null; - } - } - - private static class FileInfo { - public final String name; - public final boolean hasThumbnail; - - FileInfo(String name, boolean hasThumbnail) { - this.name = name; - this.hasThumbnail = hasThumbnail; - } - } } diff --git a/java/src/com/android/intentresolver/contentpreview/FileInfo.kt b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt new file mode 100644 index 00000000..527bfc8e --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/FileInfo.kt @@ -0,0 +1,48 @@ +/* + * 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.contentpreview + +import android.net.Uri + +internal class FileInfo private constructor( + val uri: Uri, + val name: String?, + val previewUri: Uri?, + val mimeType: String? +) { + class Builder(val uri: Uri) { + var name: String = "" + private set + var previewUri: Uri? = null + private set + var mimeType: String? = null + private set + + fun withName(name: String): Builder = apply { + this.name = name + } + + fun withPreviewUri(uri: Uri?): Builder = apply { + previewUri = uri + } + + fun withMimeType(mimeType: String?): Builder = apply { + this.mimeType = mimeType + } + + fun build(): FileInfo = FileInfo(uri, name, previewUri, mimeType) + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index db26ab1b..5f3bdf40 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -39,7 +39,8 @@ import com.android.intentresolver.R; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; -import com.android.intentresolver.widget.ImagePreviewView; +import com.android.intentresolver.widget.ChooserImagePreviewView; +import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import java.util.ArrayList; import java.util.List; @@ -51,7 +52,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { private final CharSequence mText; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; - private final ImagePreviewView.TransitionElementStatusCallback mTransitionElementStatusCallback; + private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; ImageContentPreviewUi( @@ -59,7 +60,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @Nullable CharSequence text, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback, + TransitionElementStatusCallback transitionElementStatusCallback, FeatureFlagRepository featureFlagRepository) { mImageUris = imageUris; mText = text; @@ -87,7 +88,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); - ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); + ChooserImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); if (actionRow != null) { @@ -103,7 +104,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { TAG, "Attempted to display image preview area with zero" + " available images detected in EXTRA_STREAM list"); - ((View) imagePreview).setVisibility(View.GONE); + imagePreview.setVisibility(View.GONE); mTransitionElementStatusCallback.onAllTransitionElementsReady(); return contentPreviewLayout; } @@ -129,14 +130,10 @@ class ImageContentPreviewUi extends ContentPreviewUi { return actions; } - private ImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) { + private ChooserImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) { ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub); if (stub != null) { - int layoutId = - mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW) - ? R.layout.scrollable_image_preview_view - : R.layout.chooser_image_preview_view; - stub.setLayoutResource(layoutId); + stub.setLayoutResource(R.layout.chooser_image_preview_view); stub.inflate(); } return previewLayout.findViewById( diff --git a/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java new file mode 100644 index 00000000..5172dd29 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/MimeTypeClassifier.java @@ -0,0 +1,38 @@ +/* + * 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.contentpreview; + +import android.content.ClipDescription; + +import androidx.annotation.Nullable; + +/** + * Testing shim to specify whether a given mime type is considered to be an "image." + * + * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests, + * then migrate {@link com.android.intentresolver.ChooserActivity#isImageType(String)} into this + * class. + */ +public interface MimeTypeClassifier { + /** @return whether the specified {@code mimeType} is classified as an "image" type. */ + boolean isImageType(@Nullable String mimeType); + + /** @return whether the specified {@code mimeType} is classified as an "video" type */ + default boolean isVideoType(@Nullable String mimeType) { + return ClipDescription.compareMimeTypes(mimeType, "video/*"); + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java new file mode 100644 index 00000000..ee24d18f --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -0,0 +1,202 @@ +/* + * 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.contentpreview; + +import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; + +import android.content.res.Resources; +import android.text.TextUtils; +import android.text.util.Linkify; +import android.transition.TransitionManager; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.widget.CheckBox; +import android.widget.TextView; + +import androidx.annotation.LayoutRes; +import androidx.annotation.Nullable; + +import com.android.intentresolver.ImageLoader; +import com.android.intentresolver.R; +import com.android.intentresolver.flags.FeatureFlagRepository; +import com.android.intentresolver.flags.Flags; +import com.android.intentresolver.widget.ActionRow; +import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; +import com.android.intentresolver.widget.ScrollableImagePreviewView; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; + +class UnifiedContentPreviewUi extends ContentPreviewUi { + private final List mFiles; + @Nullable + private final CharSequence mText; + private final ChooserContentPreviewUi.ActionFactory mActionFactory; + private final ImageLoader mImageLoader; + private final MimeTypeClassifier mTypeClassifier; + private final TransitionElementStatusCallback mTransitionElementStatusCallback; + private final FeatureFlagRepository mFeatureFlagRepository; + + UnifiedContentPreviewUi( + List files, + @Nullable CharSequence text, + ChooserContentPreviewUi.ActionFactory actionFactory, + ImageLoader imageLoader, + MimeTypeClassifier typeClassifier, + TransitionElementStatusCallback transitionElementStatusCallback, + FeatureFlagRepository featureFlagRepository) { + mFiles = files; + mText = text; + mActionFactory = actionFactory; + mImageLoader = imageLoader; + mTypeClassifier = typeClassifier; + mTransitionElementStatusCallback = transitionElementStatusCallback; + mFeatureFlagRepository = featureFlagRepository; + + mImageLoader.prePopulate(mFiles.stream() + .map(FileInfo::getPreviewUri) + .filter(Objects::nonNull) + .toList()); + } + + @Override + public int getType() { + return CONTENT_PREVIEW_IMAGE; + } + + @Override + public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { + ViewGroup layout = displayInternal(layoutInflater, parent); + displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + return layout; + } + + private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { + @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); + ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( + R.layout.chooser_grid_preview_image, parent, false); + ScrollableImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); + + final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); + if (actionRow != null) { + actionRow.setActions( + createActions( + createImagePreviewActions(), + mActionFactory.createCustomActions(), + mFeatureFlagRepository)); + } + + if (mFiles.size() == 0) { + Log.i( + TAG, + "Attempted to display image preview area with zero" + + " available images detected in EXTRA_STREAM list"); + imagePreview.setVisibility(View.GONE); + mTransitionElementStatusCallback.onAllTransitionElementsReady(); + return contentPreviewLayout; + } + + setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); + imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); + List previews = mFiles.stream() + .filter(fileInfo -> fileInfo.getPreviewUri() != null) + .map(fileInfo -> + new ScrollableImagePreviewView.Preview( + getPreviewType(fileInfo.getMimeType()), + fileInfo.getPreviewUri())) + .toList(); + imagePreview.setPreviews( + previews, + mFiles.size() - previews.size(), + mImageLoader); + + return contentPreviewLayout; + } + + private List createImagePreviewActions() { + ArrayList actions = new ArrayList<>(2); + //TODO: add copy action; + ActionRow.Action action = mActionFactory.createNearbyButton(); + if (action != null) { + actions.add(action); + } + action = mActionFactory.createEditButton(); + if (action != null) { + actions.add(action); + } + return actions; + } + + private ScrollableImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) { + ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub); + if (stub != null) { + stub.setLayoutResource(R.layout.scrollable_image_preview_view); + stub.inflate(); + } + return previewLayout.findViewById( + com.android.internal.R.id.content_preview_image_area); + } + + private void setTextInImagePreviewVisibility( + ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { + int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) + && !TextUtils.isEmpty(mText) + ? View.VISIBLE + : View.GONE; + + final TextView textView = contentPreview + .requireViewById(com.android.internal.R.id.content_preview_text); + CheckBox actionView = contentPreview + .requireViewById(R.id.include_text_action); + textView.setVisibility(visibility); + boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString()); + textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); + textView.setText(mText); + + if (visibility == View.VISIBLE) { + final int[] actionLabels = isLink + ? new int[] { R.string.include_link, R.string.exclude_link } + : new int[] { R.string.include_text, R.string.exclude_text }; + final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); + actionView.setChecked(true); + actionView.setText(actionLabels[1]); + shareTextAction.accept(false); + actionView.setOnCheckedChangeListener((view, isChecked) -> { + view.setText(actionLabels[isChecked ? 1 : 0]); + TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + shareTextAction.accept(!isChecked); + }); + } + actionView.setVisibility(visibility); + } + + private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { + if (mTypeClassifier.isImageType(mimeType)) { + return ScrollableImagePreviewView.PreviewType.Image; + } + if (mTypeClassifier.isVideoType(mimeType)) { + return ScrollableImagePreviewView.PreviewType.Video; + } + return ScrollableImagePreviewView.PreviewType.File; + } +} diff --git a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt index ca94a95d..6273296d 100644 --- a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt @@ -74,7 +74,7 @@ class ChooserImagePreviewView : RelativeLayout, ImagePreviewView { transitionStatusElementCallback = callback } - override fun setImages(uris: List, imageLoader: ImageLoader) { + fun setImages(uris: List, imageLoader: ImageLoader) { loadImageJob?.cancel() loadImageJob = coroutineScope.launch { when (uris.size) { diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt index a166ef27..8813adca 100644 --- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt @@ -23,7 +23,6 @@ internal typealias ImageLoader = suspend (Uri) -> Bitmap? interface ImagePreviewView { fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) - fun setImages(uris: List, imageLoader: ImageLoader) /** * [ImagePreviewView] progressively prepares views for shared element transition and reports diff --git a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java index 8538041b..8ca6ed14 100644 --- a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java +++ b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java @@ -17,6 +17,7 @@ package com.android.intentresolver.widget; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; @@ -52,7 +53,17 @@ public class RoundedRectImageView extends ImageView { public RoundedRectImageView( Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius); + + final TypedArray a = context.obtainStyledAttributes( + attrs, + R.styleable.RoundedRectImageView, + defStyleAttr, + 0); + mRadius = a.getDimensionPixelSize(R.styleable.RoundedRectImageView_radius, -1); + if (mRadius < 0) { + mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius); + } + a.recycle(); mOverlayPaint.setColor(0x99000000); mOverlayPaint.setStyle(Paint.Style.FILL); diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 467c404a..c02a10a2 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -20,11 +20,13 @@ import android.content.Context import android.graphics.Rect import android.net.Uri import android.util.AttributeSet +import android.util.PluralsMessageFormatter import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView +import android.widget.TextView import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.intentresolver.R @@ -33,11 +35,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.plus +import kotlin.math.sign private const val TRANSITION_NAME = "screenshot_preview_image" +private const val PLURALS_COUNT = "count" class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { constructor(context: Context) : this(context, null) @@ -66,41 +69,64 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { previewAdapter.transitionStatusElementCallback = callback } - override fun setImages(uris: List, imageLoader: ImageLoader) { - previewAdapter.setImages(uris, imageLoader) + fun setPreviews(previews: List, otherItemCount: Int, imageLoader: ImageLoader) = + previewAdapter.setPreviews(previews, otherItemCount, imageLoader) + + class Preview(val type: PreviewType, val uri: Uri) + enum class PreviewType { + Image, Video, File } private class Adapter(private val context: Context) : RecyclerView.Adapter() { - private val uris = ArrayList() + private val previews = ArrayList() private var imageLoader: ImageLoader? = null + private var firstImagePos = -1 var transitionStatusElementCallback: TransitionElementStatusCallback? = null + private var otherItemCount = 0 - fun setImages(uris: List, imageLoader: ImageLoader) { - this.uris.clear() - this.uris.addAll(uris) + fun setPreviews( + previews: List, otherItemCount: Int, imageLoader: ImageLoader + ) { + this.previews.clear() + this.previews.addAll(previews) this.imageLoader = imageLoader + firstImagePos = previews.indexOfFirst { it.type == PreviewType.Image } + this.otherItemCount = maxOf(0, otherItemCount) notifyDataSetChanged() } override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder { - return ViewHolder( - LayoutInflater.from(context) - .inflate(R.layout.image_preview_image_item, parent, false) - ) + val view = LayoutInflater.from(context).inflate(itemType, parent, false); + return if (itemType == R.layout.image_preview_other_item) { + OtherItemViewHolder(view) + } else { + PreviewViewHolder(view) + } } - override fun getItemCount(): Int = uris.size + override fun getItemCount(): Int = previews.size + otherItemCount.sign + + override fun getItemViewType(position: Int): Int { + return if (position == previews.size) { + R.layout.image_preview_other_item + } else { + R.layout.image_preview_image_item + } + } override fun onBindViewHolder(vh: ViewHolder, position: Int) { - vh.bind( - uris[position], - imageLoader ?: error("ImageLoader is missing"), - if (position == 0 && transitionStatusElementCallback != null) { - this::onTransitionElementReady - } else { - null - } - ) + when (vh) { + is OtherItemViewHolder -> vh.bind(otherItemCount) + is PreviewViewHolder -> vh.bind( + previews[position], + imageLoader ?: error("ImageLoader is missing"), + if (position == firstImagePos && transitionStatusElementCallback != null) { + this::onTransitionElementReady + } else { + null + } + ) + } } override fun onViewRecycled(vh: ViewHolder) { @@ -121,12 +147,18 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + private sealed class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + abstract fun unbind() + } + + private class PreviewViewHolder(view: View) : ViewHolder(view) { private val image = view.requireViewById(R.id.image) + private val badgeFrame = view.requireViewById(R.id.badge_frame) + private val badge = view.requireViewById(R.id.badge) private var scope: CoroutineScope? = null fun bind( - uri: Uri, + preview: Preview, imageLoader: ImageLoader, previewReadyCallback: ((String) -> Unit)? ) { @@ -136,26 +168,35 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } else { null } + badgeFrame.visibility = when (preview.type) { + PreviewType.Image -> View.GONE + PreviewType.Video -> { + badge.setImageResource(R.drawable.ic_file_video) + View.VISIBLE + } + else -> { + badge.setImageResource(R.drawable.chooser_file_generic) + View.VISIBLE + } + } resetScope().launch { - loadImage(uri, imageLoader, previewReadyCallback) + loadImage(preview.uri, imageLoader) + if (preview.type == PreviewType.Image) { + previewReadyCallback?.let { callback -> + image.waitForPreDraw() + callback(TRANSITION_NAME) + } + } } } - private suspend fun loadImage( - uri: Uri, - imageLoader: ImageLoader, - previewReadyCallback: ((String) -> Unit)? - ) { + private suspend fun loadImage(uri: Uri, imageLoader: ImageLoader) { val bitmap = runCatching { // it's expected for all loading/caching optimizations to be implemented by the // loader imageLoader(uri) }.getOrNull() image.setImageBitmap(bitmap) - previewReadyCallback?.let { callback -> - image.waitForPreDraw() - callback(TRANSITION_NAME) - } } private fun resetScope(): CoroutineScope = @@ -164,13 +205,27 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { scope = it } - fun unbind() { + override fun unbind() { scope?.cancel() scope = null } } - private class SpacingDecoration(private val margin: Int) : RecyclerView.ItemDecoration() { + private class OtherItemViewHolder(view: View) : ViewHolder(view) { + private val label = view.requireViewById(R.id.label) + + fun bind(count: Int) { + label.text = PluralsMessageFormatter.format( + itemView.context.resources, + mapOf(PLURALS_COUNT to count), + R.string.other_files + ) + } + + override fun unbind() = Unit + } + + private class SpacingDecoration(private val margin: Int) : ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) { outRect.set(margin, 0, margin, 0) } diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index d870a8c2..ba89c367 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -39,7 +39,7 @@ import java.util.function.Consumer private const val PROVIDER_NAME = "org.pkg.app" class ChooserContentPreviewUiTest { private val contentResolver = mock() - private val imageClassifier = ChooserContentPreviewUi.ImageMimeTypeClassifier { mimeType -> + private val imageClassifier = MimeTypeClassifier { mimeType -> mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*") } private val imageLoader = object : ImageLoader { @@ -123,7 +123,7 @@ class ChooserContentPreviewUiTest { } @Test - fun test_ChooserContentPreview_single_non_image_uri_to_file_preview() { + fun test_ChooserContentPreview_single_uri_without_preview_to_file_preview() { val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) @@ -143,6 +143,29 @@ class ChooserContentPreviewUiTest { verify(transitionCallback, times(1)).onAllTransitionElementsReady() } + @Test + fun test_ChooserContentPreview_single_uri_with_preview_to_image_preview() { + val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenReturn(arrayOf("application/pdf", "image/png")) + val testSubject = ChooserContentPreviewUi( + targetIntent, + contentResolver, + imageClassifier, + imageLoader, + actionFactory, + transitionCallback, + featureFlagRepository + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + verify(transitionCallback, never()).onAllTransitionElementsReady() + } + @Test fun test_ChooserContentPreview_multiple_image_uri_to_image_preview() { val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png") @@ -173,7 +196,7 @@ class ChooserContentPreviewUiTest { } @Test - fun test_ChooserContentPreview_some_non_image_uri_to_file_preview() { + fun test_ChooserContentPreview_some_non_image_uri_to_image_preview() { val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png") val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf") val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { @@ -197,7 +220,67 @@ class ChooserContentPreviewUiTest { featureFlagRepository ) assertThat(testSubject.preferredContentPreview) - .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + verify(transitionCallback, never()).onAllTransitionElementsReady() + } + + @Test + fun test_ChooserContentPreview_some_non_image_uri_with_preview_to_image_preview() { + val uri1 = Uri.parse("content://$PROVIDER_NAME/test.mp4") + val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList().apply { + add(uri1) + add(uri2) + } + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4") + whenever(contentResolver.getStreamTypes(uri1, "*/*")) + .thenReturn(arrayOf("image/png")) + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val testSubject = ChooserContentPreviewUi( + targetIntent, + contentResolver, + imageClassifier, + imageLoader, + actionFactory, + transitionCallback, + featureFlagRepository + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) + verify(transitionCallback, never()).onAllTransitionElementsReady() + } + + @Test + fun test_ChooserContentPreview_all_non_image_uris_without_preview_to_file_preview() { + val uri1 = Uri.parse("content://$PROVIDER_NAME/test.html") + val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + putExtra( + Intent.EXTRA_STREAM, + ArrayList().apply { + add(uri1) + add(uri2) + } + ) + } + whenever(contentResolver.getType(uri1)).thenReturn("text/html") + whenever(contentResolver.getType(uri2)).thenReturn("application/pdf") + val testSubject = ChooserContentPreviewUi( + targetIntent, + contentResolver, + imageClassifier, + imageLoader, + actionFactory, + transitionCallback, + featureFlagRepository + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) verify(transitionCallback, times(1)).onAllTransitionElementsReady() } } -- cgit v1.2.3-59-g8ed1b From 3c3755b4ea535df1b5f2a9f3e57b235b660e1fcd Mon Sep 17 00:00:00 2001 From: 1 Date: Thu, 16 Mar 2023 21:30:03 +0000 Subject: Update text preview UI to mocks Still more details to refine, but structurally this is closer. - Merge text preview, title and icon into one space. - Shaded background instead of border (using a themed color, though probably not the right one yet, waiting on UX) - Less padding around text preview area. - Give title 1 line, text preview 3. - No border around icon image. - Move actions to the bottom of the preview. Test: atest IntentResolverUnitTests Bug: 273784361 Bug: 271158656 Change-Id: I6ee364fa699a252519ea51bce47792ebecece817 --- .../drawable/chooser_content_preview_rounded.xml | 33 +++++++++ java/res/layout/chooser_grid_preview_text.xml | 84 ++++++++++------------ .../android/intentresolver/ChooserActivity.java | 2 - .../contentpreview/ContentPreviewUi.java | 4 +- .../contentpreview/TextContentPreviewUi.java | 43 ++++++----- 5 files changed, 94 insertions(+), 72 deletions(-) create mode 100644 java/res/drawable/chooser_content_preview_rounded.xml (limited to 'java/src') diff --git a/java/res/drawable/chooser_content_preview_rounded.xml b/java/res/drawable/chooser_content_preview_rounded.xml new file mode 100644 index 00000000..490a529a --- /dev/null +++ b/java/res/drawable/chooser_content_preview_rounded.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml index f25eca9a..f521e31d 100644 --- a/java/res/layout/chooser_grid_preview_text.xml +++ b/java/res/layout/chooser_grid_preview_text.xml @@ -27,29 +27,59 @@ android:background="?android:attr/colorBackground"> + android:padding="@dimen/chooser_edge_margin_normal" + android:minHeight="80dp" + android:background="@drawable/chooser_content_preview_rounded" + android:id="@+id/text_preview_layout"> + + + + - - - - - - - - - - diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 37a17e79..2a73c42a 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -652,8 +652,6 @@ public class ChooserActivity extends ResolverActivity implements parent = parent == null ? getWindow().getDecorView() : parent; - updateLayoutWidth(com.android.internal.R.id.content_preview_text_layout, width, parent); - updateLayoutWidth(com.android.internal.R.id.content_preview_title_layout, width, parent); updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent); } diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 96f1c376..79444b4e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -30,6 +30,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.view.animation.DecelerateInterpolator; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.LayoutRes; @@ -38,7 +39,6 @@ import com.android.intentresolver.R; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; -import com.android.intentresolver.widget.RoundedRectImageView; import java.util.ArrayList; import java.util.List; @@ -99,7 +99,7 @@ abstract class ContentPreviewUi { return true; } - protected static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) { + protected static void updateViewWithImage(ImageView imageView, Bitmap image) { if (image == null) { imageView.setVisibility(View.GONE); return; diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index 7901e4cb..e143954e 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -92,35 +92,34 @@ class TextContentPreviewUi extends ContentPreviewUi { if (mSharingText == null) { contentPreviewLayout - .findViewById(com.android.internal.R.id.content_preview_text_layout) + .findViewById(R.id.text_preview_layout) .setVisibility(View.GONE); - } else { - TextView textView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_text); - textView.setText(mSharingText); + return contentPreviewLayout; } + TextView textView = contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_text); + textView.setText(mSharingText); + + TextView previewTitleView = contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_title); if (TextUtils.isEmpty(mPreviewTitle)) { - contentPreviewLayout - .findViewById(com.android.internal.R.id.content_preview_title_layout) - .setVisibility(View.GONE); + previewTitleView.setVisibility(View.GONE); } else { - TextView previewTitleView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_title); previewTitleView.setText(mPreviewTitle); + } - ImageView previewThumbnailView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_thumbnail); - if (!validForContentPreview(mPreviewThumbnail)) { - previewThumbnailView.setVisibility(View.GONE); - } else { - mImageLoader.loadImage( - mPreviewThumbnail, - (bitmap) -> updateViewWithImage( - contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_thumbnail), - bitmap)); - } + ImageView previewThumbnailView = contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_thumbnail); + if (!validForContentPreview(mPreviewThumbnail)) { + previewThumbnailView.setVisibility(View.GONE); + } else { + mImageLoader.loadImage( + mPreviewThumbnail, + (bitmap) -> updateViewWithImage( + contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_thumbnail), + bitmap)); } return contentPreviewLayout; -- cgit v1.2.3-59-g8ed1b From 9f90b598713528b5a216c771acee7753ab10d953 Mon Sep 17 00:00:00 2001 From: Govinda Wasserman Date: Thu, 16 Mar 2023 13:22:09 -0400 Subject: Adds a reciever for pin migration data The receiver can only be contacted by a sender with the ADD_CHOOSER_PINS permission to prevent a 3rd party from abusing the feature to inject a pin. Test: atest ChooserPinMigrationReceiverTest BUG: 223249318 Change-Id: I3d796b40c50b64848f86398e532c3e18b0937e61 --- AndroidManifest.xml | 10 ++- .../intentresolver/ChooserPinMigrationReceiver.kt | 55 +++++++++++++++ .../ChooserPinMigrationReceiverTest.kt | 78 ++++++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt create mode 100644 java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt (limited to 'java/src') diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fc99c0b2..a3acc8a6 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -22,7 +22,6 @@ android:versionName="2021-11" coreApp="true"> - @@ -32,6 +31,7 @@ + @@ -71,6 +71,14 @@ + + + + + + diff --git a/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt b/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt new file mode 100644 index 00000000..a3ba2192 --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt @@ -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.intentresolver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.util.Log + +/** + * Broadcast receiver for receiving Chooser pin data from the legacy chooser. + * + * Unions the legacy pins with any existing pins. This receiver is protected by the ADD_CHOOSER_PINS + * permission. The receiver is required to have the RECEIVE_CHOOSER_PIN_MIGRATION to receive the + * broadcast. + */ +class ChooserPinMigrationReceiver( + private val pinnedSharedPrefsProvider: (Context) -> SharedPreferences = + { context -> ChooserActivity.getPinnedSharedPrefs(context) }, +) : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val bundle = intent.extras ?: return + Log.i(TAG, "Starting migration") + + val prefsEditor = pinnedSharedPrefsProvider.invoke(context).edit() + bundle.keySet().forEach { key -> + if(bundle.getBoolean(key)) { + prefsEditor.putBoolean(key, true) + } + } + prefsEditor.apply() + + Log.i(TAG, "Migration complete") + } + + companion object { + private const val TAG = "ChooserPinMigrationReceiver" + } +} \ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt b/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt new file mode 100644 index 00000000..1daee137 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt @@ -0,0 +1,78 @@ +/* + * 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 + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations + +@RunWith(AndroidJUnit4::class) +class ChooserPinMigrationReceiverTest { + + private lateinit var chooserPinMigrationReceiver: ChooserPinMigrationReceiver + + @Mock private lateinit var mockContext: Context + @Mock private lateinit var mockSharedPreferences: SharedPreferences + @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) + + chooserPinMigrationReceiver = ChooserPinMigrationReceiver { mockSharedPreferences } + } + + @Test + fun onReceive_addsReceivedPins() { + // Arrange + val intent = Intent().apply { + putExtra("TestPackage/TestClass", true) + } + + // Act + chooserPinMigrationReceiver.onReceive(mockContext, intent) + + // Assert + verify(mockSharedPreferencesEditor).putBoolean(eq("TestPackage/TestClass"), eq(true)) + verify(mockSharedPreferencesEditor).apply() + } + + @Test + fun onReceive_ignoresUnpinnedEntries() { + // Arrange + val intent = Intent().apply { + putExtra("TestPackage/TestClass", false) + } + + // Act + chooserPinMigrationReceiver.onReceive(mockContext, intent) + + // Assert + verify(mockSharedPreferencesEditor).apply() + verifyNoMoreInteractions(mockSharedPreferencesEditor) + } +} \ No newline at end of file -- cgit v1.2.3-59-g8ed1b From 19b5edd72865206184068604804d5eebf83f896d Mon Sep 17 00:00:00 2001 From: 1 Date: Sat, 18 Mar 2023 14:45:17 +0000 Subject: Add headline to sharesheet. Move modify share UI to upper right. Bug: 273788379 Bug: 271159515 Test: atest IntentResolverUnitTests Change-Id: Ia1099c60a4bf9034b6a34b48a948ca1b5280a9aa --- java/res/layout/chooser_grid_preview_file.xml | 10 +-- java/res/layout/chooser_grid_preview_image.xml | 16 +--- java/res/layout/chooser_grid_preview_text.xml | 10 +-- java/res/layout/chooser_headline_row.xml | 57 ++++++++++++ java/res/values/strings.xml | 30 +++++++ .../android/intentresolver/ChooserActivity.java | 4 +- .../contentpreview/ChooserContentPreviewUi.java | 42 ++++++--- .../contentpreview/ContentPreviewUi.java | 17 +++- .../contentpreview/FileContentPreviewUi.java | 9 +- .../contentpreview/HeadlineGenerator.kt | 35 ++++++++ .../contentpreview/HeadlineGeneratorImpl.kt | 67 ++++++++++++++ .../contentpreview/ImageContentPreviewUi.java | 21 ++++- .../contentpreview/TextContentPreviewUi.java | 9 +- .../contentpreview/UnifiedContentPreviewUi.java | 100 ++++++++++++++------- .../UnbundledChooserActivityTest.java | 39 ++++++++ .../contentpreview/ChooserContentPreviewUiTest.kt | 28 ++++-- .../contentpreview/HeadlineGeneratorImplTest.kt | 49 ++++++++++ 17 files changed, 450 insertions(+), 93 deletions(-) create mode 100644 java/res/layout/chooser_headline_row.xml create mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt create mode 100644 java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 6ba06b3d..036c5318 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -27,6 +27,8 @@ android:paddingBottom="@dimen/chooser_view_spacing" android:background="?android:attr/colorBackground"> + + - - - + - - + + - - + + + + + + + + + + + diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index d1c97c7e..f38666e4 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -137,6 +137,36 @@ } + + Sharing text + + Sharing link + + {count, plural, + =1 {Sharing image} + other {Sharing # images} + } + + + {count, plural, + =1 {Sharing video} + other {Sharing # videos} + } + + + {count, plural, + =1 {Sharing # item} + other {Sharing # items} + } + + + Sharing image with text + + Sharing image with link + No recommended people to share with diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 2a73c42a..1419bca7 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -84,6 +84,7 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; +import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.flags.Flags; @@ -291,7 +292,8 @@ public class ChooserActivity extends ResolverActivity implements createPreviewImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, - mFeatureFlagRepository); + mFeatureFlagRepository, + new HeadlineGeneratorImpl(this)); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 08cebf68..047a10fd 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -103,7 +103,8 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mContentPreviewUi = createContentPreview( targetIntent, @@ -112,7 +113,8 @@ public final class ChooserContentPreviewUi { imageLoader, actionFactory, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { transitionElementStatusCallback.onAllTransitionElementsReady(); } @@ -125,7 +127,8 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { /* In {@link android.content.Intent#getType}, the app may specify a very general mime type * that broadly covers all data being shared, such as {@literal *}/* when sending an image @@ -139,12 +142,20 @@ public final class ChooserContentPreviewUi { if (!(isSend || isSendMultiple) || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) { return createTextPreview( - targetIntent, actionFactory, imageLoader, featureFlagRepository); + targetIntent, + actionFactory, + imageLoader, + featureFlagRepository, + headlineGenerator); } List uris = extractContentUris(targetIntent); if (uris.isEmpty()) { return createTextPreview( - targetIntent, actionFactory, imageLoader, featureFlagRepository); + targetIntent, + actionFactory, + imageLoader, + featureFlagRepository, + headlineGenerator); } ArrayList files = new ArrayList<>(uris.size()); int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); @@ -153,7 +164,8 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { return new UnifiedContentPreviewUi( @@ -163,14 +175,16 @@ public final class ChooserContentPreviewUi { imageLoader, typeClassifier, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } if (previewCount < uris.size()) { return new FileContentPreviewUi( files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up // here. To preserve the legacy behavior, before using it, check that all uris are images. @@ -180,7 +194,8 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } } return new ImageContentPreviewUi( @@ -192,7 +207,8 @@ public final class ChooserContentPreviewUi { actionFactory, imageLoader, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } public int getPreferredContentPreview() { @@ -307,7 +323,8 @@ public final class ChooserContentPreviewUi { Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); ClipData previewData = targetIntent.getClipData(); @@ -324,7 +341,8 @@ public final class ChooserContentPreviewUi { previewThumbnail, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } private static List extractContentUris(Intent targetIntent) { diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 79444b4e..a6bc2164 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -114,7 +115,21 @@ abstract class ContentPreviewUi { fadeAnim.start(); } - protected static void displayPayloadReselectionAction( + protected static void displayHeadline(ViewGroup layout, String headline) { + if (layout != null) { + TextView titleView = layout.findViewById(R.id.headline); + if (titleView != null) { + if (!TextUtils.isEmpty(headline)) { + titleView.setText(headline); + titleView.setVisibility(View.VISIBLE); + } else { + titleView.setVisibility(View.GONE); + } + } + } + } + + protected static void displayModifyShareAction( ViewGroup layout, ChooserContentPreviewUi.ActionFactory actionFactory, FeatureFlagRepository featureFlagRepository) { diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 2c5def8b..52e20cf0 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -45,16 +45,19 @@ class FileContentPreviewUi extends ContentPreviewUi { private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; FileContentPreviewUi( List files, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mFiles = files; mActionFactory = actionFactory; mImageLoader = imageLoader; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; } @Override @@ -65,7 +68,7 @@ class FileContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(resources, layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -77,6 +80,8 @@ class FileContentPreviewUi extends ContentPreviewUi { final int uriCount = mFiles.size(); + displayHeadline(contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); + if (uriCount == 0) { contentPreviewLayout.setVisibility(View.GONE); Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM," diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt new file mode 100644 index 00000000..e32bb5c4 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt @@ -0,0 +1,35 @@ +/* + * 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.contentpreview + +private const val PLURALS_COUNT = "count" + +/** + * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief + * description of the content being shared. + */ +interface HeadlineGenerator { + fun getTextHeadline(text: CharSequence): String + + fun getImageWithTextHeadline(text: CharSequence): String + + fun getImagesHeadline(count: Int): String + + fun getVideosHeadline(count: Int): String + + fun getItemsHeadline(count: Int): String +} diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt new file mode 100644 index 00000000..ae44294c --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -0,0 +1,67 @@ +/* + * 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.contentpreview + +import android.content.Context +import com.android.intentresolver.R +import android.util.PluralsMessageFormatter + +private const val PLURALS_COUNT = "count" + +/** + * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief + * description of the content being shared. + */ +class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator { + override fun getTextHeadline(text: CharSequence): String { + if (text.toString().isHttpUri()) { + return context.getString(R.string.sharing_link) + } + return context.getString(R.string.sharing_text) + } + + override fun getImageWithTextHeadline(text: CharSequence): String { + if (text.toString().isHttpUri()) { + return context.getString(R.string.sharing_image_with_link) + } + return context.getString(R.string.sharing_image_with_text) + } + + override fun getImagesHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_images + ) + } + + override fun getVideosHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_videos + ) + } + + override fun getItemsHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_items + ) + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index 5f3bdf40..f2c0564a 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -54,6 +54,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; ImageContentPreviewUi( List imageUris, @@ -61,13 +62,15 @@ class ImageContentPreviewUi extends ContentPreviewUi { ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mImageUris = imageUris; mText = text; mActionFactory = actionFactory; mImageLoader = imageLoader; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mImageUris); } @@ -80,7 +83,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -113,6 +116,8 @@ class ImageContentPreviewUi extends ContentPreviewUi { imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); imagePreview.setImages(mImageUris, mImageLoader); + updateHeadline(contentPreviewLayout); + return contentPreviewLayout; } @@ -140,6 +145,17 @@ class ImageContentPreviewUi extends ContentPreviewUi { com.android.internal.R.id.content_preview_image_area); } + private void updateHeadline(ViewGroup contentPreview) { + CheckBox includeTextCheckbox = contentPreview.requireViewById(R.id.include_text_action); + if (includeTextCheckbox.getVisibility() == View.VISIBLE + && includeTextCheckbox.isChecked()) { + displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); + } else { + displayHeadline( + contentPreview, mHeadlineGenerator.getImagesHeadline(mImageUris.size())); + } + } + private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) @@ -169,6 +185,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); shareTextAction.accept(!isChecked); + updateHeadline(contentPreview); }); } actionView.setVisibility(visibility); diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index e143954e..d0cba5bb 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -46,6 +46,7 @@ class TextContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; TextContentPreviewUi( @Nullable CharSequence sharingText, @@ -53,13 +54,15 @@ class TextContentPreviewUi extends ContentPreviewUi { @Nullable Uri previewThumbnail, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mSharingText = sharingText; mPreviewTitle = previewTitle; mPreviewThumbnail = previewThumbnail; mImageLoader = imageLoader; mActionFactory = actionFactory; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; } @Override @@ -70,7 +73,7 @@ class TextContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -122,6 +125,8 @@ class TextContentPreviewUi extends ContentPreviewUi { bitmap)); } + displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText)); + return contentPreviewLayout; } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index ee24d18f..b9c64c6f 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -55,6 +55,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; UnifiedContentPreviewUi( List files, @@ -63,7 +64,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mFiles = files; mText = text; mActionFactory = actionFactory; @@ -71,6 +73,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mFiles.stream() .map(FileInfo::getPreviewUri) @@ -86,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -115,20 +118,47 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return contentPreviewLayout; } - setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); - List previews = mFiles.stream() - .filter(fileInfo -> fileInfo.getPreviewUri() != null) - .map(fileInfo -> - new ScrollableImagePreviewView.Preview( - getPreviewType(fileInfo.getMimeType()), - fileInfo.getPreviewUri())) - .toList(); + + List previews = new ArrayList<>(); + boolean allImages = !mFiles.isEmpty(); + boolean allVideos = !mFiles.isEmpty(); + for (FileInfo fileInfo : mFiles) { + ScrollableImagePreviewView.PreviewType previewType = + getPreviewType(fileInfo.getMimeType()); + allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; + allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; + + if (fileInfo.getPreviewUri() != null) { + previews.add(new ScrollableImagePreviewView.Preview( + previewType, + fileInfo.getPreviewUri())); + } + } imagePreview.setPreviews( previews, mFiles.size() - previews.size(), mImageLoader); + if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) + && !TextUtils.isEmpty(mText) + && mFiles.size() == 1 + && allImages) { + setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); + updateTextWithImageHeadline(contentPreviewLayout); + } else { + if (allImages) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + } else if (allVideos) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); + } else { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); + } + } + return contentPreviewLayout; } @@ -156,38 +186,42 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { com.android.internal.R.id.content_preview_image_area); } + private void updateTextWithImageHeadline(ViewGroup contentPreview) { + CheckBox actionView = contentPreview.requireViewById(R.id.include_text_action); + if (actionView.getVisibility() == View.VISIBLE && actionView.isChecked()) { + displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); + } else { + displayHeadline( + contentPreview, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + } + } + private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { - int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) - && !TextUtils.isEmpty(mText) - ? View.VISIBLE - : View.GONE; - final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); CheckBox actionView = contentPreview .requireViewById(R.id.include_text_action); - textView.setVisibility(visibility); - boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString()); + textView.setVisibility(View.VISIBLE); + boolean isLink = HttpUriMatcher.isHttpUri(mText.toString()); textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); textView.setText(mText); - if (visibility == View.VISIBLE) { - final int[] actionLabels = isLink - ? new int[] { R.string.include_link, R.string.exclude_link } - : new int[] { R.string.include_text, R.string.exclude_text }; - final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); - actionView.setChecked(true); - actionView.setText(actionLabels[1]); - shareTextAction.accept(false); - actionView.setOnCheckedChangeListener((view, isChecked) -> { - view.setText(actionLabels[isChecked ? 1 : 0]); - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - shareTextAction.accept(!isChecked); - }); - } - actionView.setVisibility(visibility); + final int[] actionLabels = isLink + ? new int[] { R.string.include_link, R.string.exclude_link } + : new int[] { R.string.include_text, R.string.exclude_text }; + final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); + actionView.setChecked(true); + actionView.setText(actionLabels[1]); + shareTextAction.accept(false); + actionView.setOnCheckedChangeListener((view, isChecked) -> { + view.setText(actionLabels[isChecked ? 1 : 0]); + TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + shareTextAction.accept(!isChecked); + updateTextWithImageHeadline(contentPreview); + }); + actionView.setVisibility(View.VISIBLE); } private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 596b546e..943584a2 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -26,6 +26,7 @@ import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; @@ -98,6 +99,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; +import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -1067,6 +1069,43 @@ public class UnbundledChooserActivityTest { .check(matches(isDisplayed())); } + @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) + public void testNoTextPreviewWhenTextIsSharedWithMultipleImages() { + final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + final String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + + List resolvedComponentInfos = createResolvedComponentsForTest(2); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + Mockito.any(UserHandle.class))) + .thenReturn(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_text)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + } + @Test public void testOnCreateLogging() { Intent sendIntent = createSendTextIntent(); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 04a136b4..3a00f1f3 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -42,6 +42,7 @@ class ChooserContentPreviewUiTest { private val imageClassifier = MimeTypeClassifier { mimeType -> mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*") } + private val headlineGenerator = mock() private val imageLoader = object : ImageLoader { override fun loadImage(uri: Uri, callback: Consumer) { callback.accept(null) @@ -74,7 +75,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -94,7 +96,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -115,7 +118,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -136,7 +140,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) @@ -159,7 +164,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -188,7 +194,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -217,7 +224,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -248,7 +256,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -277,7 +286,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt new file mode 100644 index 00000000..9becce99 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt @@ -0,0 +1,49 @@ +/* + * 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.contentpreview + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Test +import org.junit.runner.RunWith +import com.google.common.truth.Truth.assertThat + +@RunWith(AndroidJUnit4::class) +class HeadlineGeneratorImplTest { + @Test + fun testHeadlineGeneration() { + val generator = HeadlineGeneratorImpl( + InstrumentationRegistry.getInstrumentation().getTargetContext()) + val str = "Some sting" + val url = "http://www.google.com" + + assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") + assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") + + assertThat(generator.getImageWithTextHeadline(str)).isEqualTo("Sharing image with text") + assertThat(generator.getImageWithTextHeadline(url)).isEqualTo("Sharing image with link") + + assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") + assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") + + assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video") + assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos") + + assertThat(generator.getItemsHeadline(1)).isEqualTo("Sharing 1 item") + assertThat(generator.getItemsHeadline(4)).isEqualTo("Sharing 4 items") + } +} -- cgit v1.2.3-59-g8ed1b From f1870096ee8ad86d45887b1de5aee70a7f93dea3 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Fri, 17 Mar 2023 02:31:04 -0700 Subject: Maintain previews aspect ratio Content preview preserves previews apspect raio (limiting between 0.4 and 2.5). ScrollableImagePreviewView now makes an initial pass over provided URIs to determine preview aspect ratio; previews that successfully loaded are progressively added to the dislay list. Previews that failed to load counted in the "+N" item at the list end. Previews are displayed in the order they are loaded (concurrently) to facilitate the fastest preview display. Preivew item height and spacings are change accroding to the latest specs. Bug: 271613784 Test: manual testing Change-Id: I430f1e7fb39c97da91bdc25914a0cb804a2b6ffa --- Android.bp | 1 + java/res/layout/image_preview_image_item.xml | 33 +-- java/res/layout/image_preview_other_item.xml | 2 +- java/res/layout/scrollable_image_preview_view.xml | 25 +- java/res/values/attrs.xml | 6 + java/res/values/dimens.xml | 1 + .../android/intentresolver/ChooserActivity.java | 4 +- .../contentpreview/UnifiedContentPreviewUi.java | 3 +- java/src/com/android/intentresolver/util/Flow.kt | 84 +++++++ .../widget/ScrollableImagePreviewView.kt | 259 +++++++++++++++++++-- .../UnbundledChooserActivityTest.java | 2 +- 11 files changed, 368 insertions(+), 52 deletions(-) create mode 100644 java/src/com/android/intentresolver/util/Flow.kt (limited to 'java/src') diff --git a/Android.bp b/Android.bp index 9238a7e2..bab33509 100644 --- a/Android.bp +++ b/Android.bp @@ -68,6 +68,7 @@ android_library { static_libs: [ "androidx.annotation_annotation", "androidx.concurrent_concurrent-futures", + "androidx-constraintlayout_constraintlayout", "androidx.recyclerview_recyclerview", "androidx.viewpager_viewpager", "androidx.lifecycle_lifecycle-common-java8", diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml index 81fa5c8e..a8a2c754 100644 --- a/java/res/layout/image_preview_image_item.xml +++ b/java/res/layout/image_preview_image_item.xml @@ -14,25 +14,30 @@ ~ limitations under the License. --> - + android:layout_width="wrap_content" + android:layout_height="@dimen/chooser_preview_image_height_tall"> - + - + diff --git a/java/res/layout/image_preview_other_item.xml b/java/res/layout/image_preview_other_item.xml index b7cc4350..07f87e3a 100644 --- a/java/res/layout/image_preview_other_item.xml +++ b/java/res/layout/image_preview_other_item.xml @@ -17,7 +17,7 @@ + android:layout_height="@dimen/chooser_preview_image_height_tall"> - + + android:layout_height="wrap_content"> + + + diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml index eba6b9b7..67acb3ae 100644 --- a/java/res/values/attrs.xml +++ b/java/res/values/attrs.xml @@ -45,4 +45,10 @@ + + + + + + diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index af90c4ef..7daa9206 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -28,6 +28,7 @@ 1dp 120dp 104dp + 192dp 200dp -1px 4dp diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 37a17e79..44000bef 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1299,7 +1299,9 @@ public class ChooserActivity extends ResolverActivity implements final int cacheSize; if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { float chooserWidth = getResources().getDimension(R.dimen.chooser_width); - float imageWidth = getResources().getDimension(R.dimen.chooser_preview_image_width); + // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView) + float imageWidth = + getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2; cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); } else { cacheSize = 3; diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index ee24d18f..c4e6feb7 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -152,8 +152,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { stub.setLayoutResource(R.layout.scrollable_image_preview_view); stub.inflate(); } - return previewLayout.findViewById( - com.android.internal.R.id.content_preview_image_area); + return previewLayout.findViewById(R.id.scrollable_image_preview); } private void setTextInImagePreviewVisibility( diff --git a/java/src/com/android/intentresolver/util/Flow.kt b/java/src/com/android/intentresolver/util/Flow.kt new file mode 100644 index 00000000..1155b9fe --- /dev/null +++ b/java/src/com/android/intentresolver/util/Flow.kt @@ -0,0 +1,84 @@ +/* + * 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.util + +import android.os.SystemClock +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.launch + +/** + * Returns a flow that mirrors the original flow, but delays values following emitted values for the + * given [periodMs]. If the original flow emits more than one value during this period, only the + * latest value is emitted. + * + * Example: + * + * ```kotlin + * flow { + * emit(1) // t=0ms + * delay(90) + * emit(2) // t=90ms + * delay(90) + * emit(3) // t=180ms + * delay(1010) + * emit(4) // t=1190ms + * delay(1010) + * emit(5) // t=2200ms + * }.throttle(1000) + * ``` + * + * produces the following emissions at the following times + * + * ```text + * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms) + * ``` + */ +// A SystemUI com.android.systemui.util.kotlin.throttle copy. +fun Flow.throttle(periodMs: Long): Flow = channelFlow { + coroutineScope { + var previousEmitTimeMs = 0L + var delayJob: Job? = null + var sendJob: Job? = null + val outerScope = this + + collect { + delayJob?.cancel() + sendJob?.join() + val currentTimeMs = SystemClock.elapsedRealtime() + val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs + val timeUntilNextEmit = maxOf(0L, periodMs - timeSinceLastEmit) + if (timeUntilNextEmit > 0L) { + // We create delayJob to allow cancellation during the delay period + delayJob = launch { + delay(timeUntilNextEmit) + sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) { + send(it) + previousEmitTimeMs = SystemClock.elapsedRealtime() + } + } + } else { + send(it) + previousEmitTimeMs = currentTimeMs + } + } + } +} diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index c02a10a2..d1b0f5b4 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -17,6 +17,7 @@ package com.android.intentresolver.widget import android.content.Context +import android.graphics.Bitmap import android.graphics.Rect import android.net.Uri import android.util.AttributeSet @@ -27,20 +28,32 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.android.intentresolver.R +import com.android.intentresolver.util.throttle import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.takeWhile +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.plus -import kotlin.math.sign +import java.util.ArrayDeque +import kotlin.math.roundToInt private const val TRANSITION_NAME = "screenshot_preview_image" private const val PLURALS_COUNT = "count" +private const val ADAPTER_UPDATE_INTERVAL_MS = 150L +private const val MIN_ASPECT_RATIO = 0.4f +private const val MIN_ASPECT_RATIO_STRING = "2:5" +private const val MAX_ASPECT_RATIO = 2.5f +private const val MAX_ASPECT_RATIO_STRING = "5:2" class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { constructor(context: Context) : this(context, null) @@ -50,14 +63,53 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { ) : super(context, attrs, defStyleAttr) { layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) adapter = Adapter(context) - val spacing = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 5f, context.resources.displayMetrics - ).toInt() - addItemDecoration(SpacingDecoration(spacing)) + + context.obtainStyledAttributes( + attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0 + ).use { a -> + var innerSpacing = a.getDimensionPixelSize( + R.styleable.ScrollableImagePreviewView_itemInnerSpacing, -1 + ) + if (innerSpacing < 0) { + innerSpacing = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 3f, context.resources.displayMetrics + ).toInt() + } + var outerSpacing = a.getDimensionPixelSize( + R.styleable.ScrollableImagePreviewView_itemOuterSpacing, -1 + ) + if (outerSpacing < 0) { + outerSpacing = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics + ).toInt() + } + addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing)) + + maxWidthHint = a.getDimensionPixelSize( + R.styleable.ScrollableImagePreviewView_maxWidthHint, -1 + ) + } } + private var batchLoader: BatchPreviewLoader? = null private val previewAdapter get() = adapter as Adapter + /** + * A hint about the maximum width this view can grow to, this helps to optimize preview + * loading. + */ + var maxWidthHint: Int = -1 + private var requestedHeight: Int = 0 + private var isMeasured = false + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + super.onMeasure(widthSpec, heightSpec) + if (!isMeasured) { + isMeasured = true + batchLoader?.loadAspectRatios(getMaxWidth(), this::calcPreviewWidth) + } + } + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { super.onLayout(changed, l, t, r, b) setOverScrollMode( @@ -69,32 +121,103 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { previewAdapter.transitionStatusElementCallback = callback } - fun setPreviews(previews: List, otherItemCount: Int, imageLoader: ImageLoader) = - previewAdapter.setPreviews(previews, otherItemCount, imageLoader) + fun setPreviews(previews: List, otherItemCount: Int, imageLoader: ImageLoader) { + previewAdapter.reset(0, imageLoader) + batchLoader?.cancel() + batchLoader = BatchPreviewLoader( + previewAdapter, + imageLoader, + previews, + otherItemCount, + ).apply { + if (isMeasured) { + loadAspectRatios(getMaxWidth(), this@ScrollableImagePreviewView::calcPreviewWidth) + } + } + } + + private fun getMaxWidth(): Int = + when { + maxWidthHint > 0 -> maxWidthHint + isLaidOut -> width + else -> measuredWidth + } + + private fun calcPreviewWidth(bitmap: Bitmap): Int { + val effectiveHeight = if (isLaidOut) height else measuredHeight + return if (bitmap.width <= 0 || bitmap.height <= 0) { + effectiveHeight + } else { + val ar = (bitmap.width.toFloat() / bitmap.height.toFloat()) + .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) + (effectiveHeight * ar).roundToInt() + } + } + + class Preview internal constructor( + val type: PreviewType, + val uri: Uri, + internal var aspectRatioString: String + ) { + constructor(type: PreviewType, uri: Uri) : this(type, uri, "1:1") + + internal var bitmap: Bitmap? = null + + internal fun updateAspectRatio(width: Int, height: Int) { + if (width <= 0 || height <= 0) return + val aspectRatio = width.toFloat() / height.toFloat() + aspectRatioString = when { + aspectRatio <= MIN_ASPECT_RATIO -> MIN_ASPECT_RATIO_STRING + aspectRatio >= MAX_ASPECT_RATIO -> MAX_ASPECT_RATIO_STRING + else -> "$width:$height" + } + } + } - class Preview(val type: PreviewType, val uri: Uri) enum class PreviewType { Image, Video, File } - private class Adapter(private val context: Context) : RecyclerView.Adapter() { + private class Adapter( + private val context: Context + ) : RecyclerView.Adapter() { private val previews = ArrayList() private var imageLoader: ImageLoader? = null private var firstImagePos = -1 + private var totalItemCount: Int = 0 + + private val hasOtherItem get() = previews.size < totalItemCount + var transitionStatusElementCallback: TransitionElementStatusCallback? = null - private var otherItemCount = 0 - fun setPreviews( - previews: List, otherItemCount: Int, imageLoader: ImageLoader - ) { - this.previews.clear() - this.previews.addAll(previews) + fun reset(totalItemCount: Int, imageLoader: ImageLoader) { this.imageLoader = imageLoader - firstImagePos = previews.indexOfFirst { it.type == PreviewType.Image } - this.otherItemCount = maxOf(0, otherItemCount) + firstImagePos = -1 + previews.clear() + this.totalItemCount = maxOf(0, totalItemCount) notifyDataSetChanged() } + fun addPreviews(newPreviews: Collection) { + if (newPreviews.isEmpty()) return + val insertPos = previews.size + val hadOtherItem = hasOtherItem + previews.addAll(newPreviews) + if (firstImagePos < 0) { + val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image } + if (pos >= 0) firstImagePos = insertPos + pos + } + notifyItemRangeInserted(insertPos, newPreviews.size) + when { + hadOtherItem && previews.size >= totalItemCount -> { + notifyItemRemoved(previews.size) + } + !hadOtherItem && previews.size < totalItemCount -> { + notifyItemInserted(previews.size) + } + } + } + override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder { val view = LayoutInflater.from(context).inflate(itemType, parent, false); return if (itemType == R.layout.image_preview_other_item) { @@ -104,7 +227,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - override fun getItemCount(): Int = previews.size + otherItemCount.sign + override fun getItemCount(): Int = previews.size + if (hasOtherItem) 1 else 0 override fun getItemViewType(position: Int): Int { return if (position == previews.size) { @@ -116,7 +239,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { override fun onBindViewHolder(vh: ViewHolder, position: Int) { when (vh) { - is OtherItemViewHolder -> vh.bind(otherItemCount) + is OtherItemViewHolder -> vh.bind(totalItemCount - previews.size) is PreviewViewHolder -> vh.bind( previews[position], imageLoader ?: error("ImageLoader is missing"), @@ -163,6 +286,9 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { previewReadyCallback: ((String) -> Unit)? ) { image.setImageDrawable(null) + (image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params -> + params.dimensionRatio = preview.aspectRatioString + } image.transitionName = if (previewReadyCallback != null) { TRANSITION_NAME } else { @@ -180,7 +306,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } resetScope().launch { - loadImage(preview.uri, imageLoader) + loadImage(preview, imageLoader) if (preview.type == PreviewType.Image) { previewReadyCallback?.let { callback -> image.waitForPreDraw() @@ -190,11 +316,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - private suspend fun loadImage(uri: Uri, imageLoader: ImageLoader) { - val bitmap = runCatching { + private suspend fun loadImage(preview: Preview, imageLoader: ImageLoader) { + val bitmap = preview.bitmap ?: runCatching { // it's expected for all loading/caching optimizations to be implemented by the // loader - imageLoader(uri) + imageLoader(preview.uri) }.getOrNull() image.setImageBitmap(bitmap) } @@ -225,9 +351,92 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { override fun unbind() = Unit } - private class SpacingDecoration(private val margin: Int) : ItemDecoration() { + private class SpacingDecoration( + private val innerSpacing: Int, + private val outerSpacing: Int + ) : ItemDecoration() { override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) { - outRect.set(margin, 0, margin, 0) + val itemCount = parent.adapter?.itemCount ?: return + val pos = parent.getChildAdapterPosition(view) + var leftMargin = if (pos == 0) outerSpacing else innerSpacing + var rightMargin = if (pos == itemCount - 1) outerSpacing else 0 + outRect.set(leftMargin, 0, rightMargin, 0) + } + } + + private class BatchPreviewLoader( + private val adapter: Adapter, + private val imageLoader: ImageLoader, + previews: List, + otherItemCount: Int, + ) { + private val pendingPreviews = ArrayDeque(previews) + private val totalItemCount = previews.size + otherItemCount + private var scope: CoroutineScope? = MainScope() + Dispatchers.Main.immediate + + fun cancel() { + scope?.cancel() + scope = null + } + + fun loadAspectRatios(maxWidth: Int, previewWidthCalculator: (Bitmap) -> Int) { + val scope = this.scope ?: return + val updates = ArrayDeque(pendingPreviews.size) + // replay 2 items to guarantee that we'd get at least one update + val reportFlow = MutableSharedFlow(replay = 2) + var isFirstUpdate = true + val updateEvent = Any() + val completedEvent = Any() + // throttle adapter updates by waiting on the channel, the channel first notified + // when enough preview elements is loaded and then periodically with a delay + scope.launch(Dispatchers.Main) { + reportFlow + .takeWhile { it !== completedEvent } + .throttle(ADAPTER_UPDATE_INTERVAL_MS) + .collect { + if (isFirstUpdate) { + isFirstUpdate = false + adapter.reset(totalItemCount, imageLoader) + } + if (updates.isNotEmpty()) { + adapter.addPreviews(updates) + updates.clear() + } + } + } + + scope.launch { + var loadedPreviewWidth = 0 + List(4) { + launch { + while (pendingPreviews.isNotEmpty()) { + val preview = pendingPreviews.poll() ?: continue + val bitmap = runCatching { + // TODO: decide on adding a timeout + imageLoader(preview.uri) + }.getOrNull() ?: continue + preview.updateAspectRatio(bitmap.width, bitmap.height) + updates.add(preview) + if (loadedPreviewWidth < maxWidth) { + loadedPreviewWidth += previewWidthCalculator(bitmap) + // cache bitmaps for the first preview items to aovid potential + // double-loading (in case those values are evicted from the image + // loader's cache) + preview.bitmap = bitmap + if (loadedPreviewWidth >= maxWidth) { + // notify that the preview now can be displayed + reportFlow.emit(updateEvent) + } + } else { + reportFlow.emit(updateEvent) + } + } + } + }.joinAll() + // in case all previews have failed to load + reportFlow.emit(updateEvent) + reportFlow.emit(completedEvent) + } } } } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 596b546e..8b9aaa60 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -1030,7 +1030,7 @@ public class UnbundledChooserActivityTest { setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_area)) + onView(withId(R.id.scrollable_image_preview)) .perform(RecyclerViewActions.scrollToLastPosition()) .check((view, exception) -> { if (exception != null) { -- cgit v1.2.3-59-g8ed1b From 4fc75124fe90daff88f213ba204a157e40ba2b96 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 21 Mar 2023 10:56:58 -0700 Subject: Do not crash on reading URI metadata Do not crash if content resolver throws an exception while trying to read an URI metadata. Bug: 273890881 Test: check that the Chooser is not failing Change-Id: I161e29246bfcb1081a500b90c88c8c06c1d0df72 --- .../contentpreview/ChooserContentPreviewUi.java | 11 +++-- .../contentpreview/ChooserContentPreviewUiTest.kt | 50 ++++++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 08cebf68..de454cfd 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -28,11 +28,11 @@ import android.content.res.Resources; import android.database.Cursor; import android.media.MediaMetadata; import android.net.Uri; -import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.Downloads; import android.provider.OpenableColumns; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -351,7 +351,8 @@ public final class ChooserContentPreviewUi { private static String getType(ContentInterface resolver, Uri uri) { try { return resolver.getType(uri); - } catch (RemoteException e) { + } catch (Throwable t) { + Log.e(ContentPreviewUi.TAG, "Failed to read content type, uri: " + uri, t); return null; } } @@ -360,7 +361,8 @@ public final class ChooserContentPreviewUi { private static Cursor query(ContentInterface resolver, Uri uri) { try { return resolver.query(uri, null, null, null); - } catch (RemoteException e) { + } catch (Throwable t) { + Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: " + uri, t); return null; } } @@ -369,7 +371,8 @@ public final class ChooserContentPreviewUi { private static String[] getStreamTypes(ContentInterface resolver, Uri uri) { try { return resolver.getStreamTypes(uri, "*/*"); - } catch (RemoteException e) { + } catch (Throwable t) { + Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: " + uri, t); return null; } } diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 04a136b4..58b8a21d 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -23,6 +23,8 @@ import android.graphics.Bitmap import android.net.Uri import com.android.intentresolver.ImageLoader import com.android.intentresolver.TestFeatureFlagRepository +import com.android.intentresolver.any +import com.android.intentresolver.anyOrNull import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory import com.android.intentresolver.flags.Flags import com.android.intentresolver.mock @@ -143,6 +145,53 @@ class ChooserContentPreviewUiTest { verify(transitionCallback, times(1)).onAllTransitionElementsReady() } + @Test + fun test_ChooserContentPreview_single_uri_crashing_getType_to_file_preview() { + val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(any())) + .thenThrow(SecurityException("Test getType() exception")) + val testSubject = ChooserContentPreviewUi( + targetIntent, + contentResolver, + imageClassifier, + imageLoader, + actionFactory, + transitionCallback, + featureFlagRepository + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + verify(transitionCallback, times(1)).onAllTransitionElementsReady() + } + + @Test + fun test_ChooserContentPreview_single_uri_crashing_metadata_to_file_preview() { + val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") + val targetIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(any())).thenReturn("application/pdf") + whenever(contentResolver.query(any(), anyOrNull(), anyOrNull(), anyOrNull())) + .thenThrow(SecurityException("Test query() exception")) + whenever(contentResolver.getStreamTypes(any(), any())) + .thenThrow(SecurityException("Test getStreamType() exception")) + val testSubject = ChooserContentPreviewUi( + targetIntent, + contentResolver, + imageClassifier, + imageLoader, + actionFactory, + transitionCallback, + featureFlagRepository + ) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + verify(transitionCallback, times(1)).onAllTransitionElementsReady() + } + @Test fun test_ChooserContentPreview_single_uri_with_preview_to_image_preview() { val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf") @@ -283,4 +332,5 @@ class ChooserContentPreviewUiTest { .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) verify(transitionCallback, times(1)).onAllTransitionElementsReady() } + } -- cgit v1.2.3-59-g8ed1b From 6aaafb277958144a39f9ff4c8328a29eb2db6e38 Mon Sep 17 00:00:00 2001 From: Remi NGUYEN VAN Date: Wed, 22 Mar 2023 07:25:59 +0000 Subject: Revert "Add headline to sharesheet." This reverts commit 19b5edd72865206184068604804d5eebf83f896d. Reason for revert: Broken build: b/274711262 Change-Id: I91e8cd61c1268d3e4e2fc2d48a7f1cedda444a4f --- java/res/layout/chooser_grid_preview_file.xml | 10 ++- java/res/layout/chooser_grid_preview_image.xml | 16 +++- java/res/layout/chooser_grid_preview_text.xml | 10 ++- java/res/layout/chooser_headline_row.xml | 57 ------------ java/res/values/strings.xml | 30 ------- .../android/intentresolver/ChooserActivity.java | 4 +- .../contentpreview/ChooserContentPreviewUi.java | 42 +++------ .../contentpreview/ContentPreviewUi.java | 17 +--- .../contentpreview/FileContentPreviewUi.java | 9 +- .../contentpreview/HeadlineGenerator.kt | 35 -------- .../contentpreview/HeadlineGeneratorImpl.kt | 67 -------------- .../contentpreview/ImageContentPreviewUi.java | 21 +---- .../contentpreview/TextContentPreviewUi.java | 9 +- .../contentpreview/UnifiedContentPreviewUi.java | 100 +++++++-------------- .../UnbundledChooserActivityTest.java | 39 -------- .../contentpreview/ChooserContentPreviewUiTest.kt | 28 ++---- .../contentpreview/HeadlineGeneratorImplTest.kt | 49 ---------- 17 files changed, 93 insertions(+), 450 deletions(-) delete mode 100644 java/res/layout/chooser_headline_row.xml delete mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt delete mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt delete mode 100644 java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 036c5318..6ba06b3d 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -27,8 +27,6 @@ android:paddingBottom="@dimen/chooser_view_spacing" android:background="?android:attr/colorBackground"> - - + + - + + + - - + + - - - - - - - - - - - diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index f38666e4..d1c97c7e 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -137,36 +137,6 @@ } - - Sharing text - - Sharing link - - {count, plural, - =1 {Sharing image} - other {Sharing # images} - } - - - {count, plural, - =1 {Sharing video} - other {Sharing # videos} - } - - - {count, plural, - =1 {Sharing # item} - other {Sharing # items} - } - - - Sharing image with text - - Sharing image with link - No recommended people to share with diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 1419bca7..2a73c42a 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -84,7 +84,6 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; -import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.flags.Flags; @@ -292,8 +291,7 @@ public class ChooserActivity extends ResolverActivity implements createPreviewImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, - mFeatureFlagRepository, - new HeadlineGeneratorImpl(this)); + mFeatureFlagRepository); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 047a10fd..08cebf68 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -103,8 +103,7 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { mContentPreviewUi = createContentPreview( targetIntent, @@ -113,8 +112,7 @@ public final class ChooserContentPreviewUi { imageLoader, actionFactory, transitionElementStatusCallback, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { transitionElementStatusCallback.onAllTransitionElementsReady(); } @@ -127,8 +125,7 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { /* In {@link android.content.Intent#getType}, the app may specify a very general mime type * that broadly covers all data being shared, such as {@literal *}/* when sending an image @@ -142,20 +139,12 @@ public final class ChooserContentPreviewUi { if (!(isSend || isSendMultiple) || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) { return createTextPreview( - targetIntent, - actionFactory, - imageLoader, - featureFlagRepository, - headlineGenerator); + targetIntent, actionFactory, imageLoader, featureFlagRepository); } List uris = extractContentUris(targetIntent); if (uris.isEmpty()) { return createTextPreview( - targetIntent, - actionFactory, - imageLoader, - featureFlagRepository, - headlineGenerator); + targetIntent, actionFactory, imageLoader, featureFlagRepository); } ArrayList files = new ArrayList<>(uris.size()); int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); @@ -164,8 +153,7 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { return new UnifiedContentPreviewUi( @@ -175,16 +163,14 @@ public final class ChooserContentPreviewUi { imageLoader, typeClassifier, transitionElementStatusCallback, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } if (previewCount < uris.size()) { return new FileContentPreviewUi( files, actionFactory, imageLoader, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up // here. To preserve the legacy behavior, before using it, check that all uris are images. @@ -194,8 +180,7 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } } return new ImageContentPreviewUi( @@ -207,8 +192,7 @@ public final class ChooserContentPreviewUi { actionFactory, imageLoader, transitionElementStatusCallback, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } public int getPreferredContentPreview() { @@ -323,8 +307,7 @@ public final class ChooserContentPreviewUi { Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); ClipData previewData = targetIntent.getClipData(); @@ -341,8 +324,7 @@ public final class ChooserContentPreviewUi { previewThumbnail, actionFactory, imageLoader, - featureFlagRepository, - headlineGenerator); + featureFlagRepository); } private static List extractContentUris(Intent targetIntent) { diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index a6bc2164..79444b4e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -24,7 +24,6 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.os.UserHandle; -import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -115,21 +114,7 @@ abstract class ContentPreviewUi { fadeAnim.start(); } - protected static void displayHeadline(ViewGroup layout, String headline) { - if (layout != null) { - TextView titleView = layout.findViewById(R.id.headline); - if (titleView != null) { - if (!TextUtils.isEmpty(headline)) { - titleView.setText(headline); - titleView.setVisibility(View.VISIBLE); - } else { - titleView.setVisibility(View.GONE); - } - } - } - } - - protected static void displayModifyShareAction( + protected static void displayPayloadReselectionAction( ViewGroup layout, ChooserContentPreviewUi.ActionFactory actionFactory, FeatureFlagRepository featureFlagRepository) { diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 52e20cf0..2c5def8b 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -45,19 +45,16 @@ class FileContentPreviewUi extends ContentPreviewUi { private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final FeatureFlagRepository mFeatureFlagRepository; - private final HeadlineGenerator mHeadlineGenerator; FileContentPreviewUi( List files, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { mFiles = files; mActionFactory = actionFactory; mImageLoader = imageLoader; mFeatureFlagRepository = featureFlagRepository; - mHeadlineGenerator = headlineGenerator; } @Override @@ -68,7 +65,7 @@ class FileContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(resources, layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -80,8 +77,6 @@ class FileContentPreviewUi extends ContentPreviewUi { final int uriCount = mFiles.size(); - displayHeadline(contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); - if (uriCount == 0) { contentPreviewLayout.setVisibility(View.GONE); Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM," diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt deleted file mode 100644 index e32bb5c4..00000000 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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.contentpreview - -private const val PLURALS_COUNT = "count" - -/** - * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief - * description of the content being shared. - */ -interface HeadlineGenerator { - fun getTextHeadline(text: CharSequence): String - - fun getImageWithTextHeadline(text: CharSequence): String - - fun getImagesHeadline(count: Int): String - - fun getVideosHeadline(count: Int): String - - fun getItemsHeadline(count: Int): String -} diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt deleted file mode 100644 index ae44294c..00000000 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.contentpreview - -import android.content.Context -import com.android.intentresolver.R -import android.util.PluralsMessageFormatter - -private const val PLURALS_COUNT = "count" - -/** - * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief - * description of the content being shared. - */ -class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator { - override fun getTextHeadline(text: CharSequence): String { - if (text.toString().isHttpUri()) { - return context.getString(R.string.sharing_link) - } - return context.getString(R.string.sharing_text) - } - - override fun getImageWithTextHeadline(text: CharSequence): String { - if (text.toString().isHttpUri()) { - return context.getString(R.string.sharing_image_with_link) - } - return context.getString(R.string.sharing_image_with_text) - } - - override fun getImagesHeadline(count: Int): String { - return PluralsMessageFormatter.format( - context.resources, - mapOf(PLURALS_COUNT to count), - R.string.sharing_images - ) - } - - override fun getVideosHeadline(count: Int): String { - return PluralsMessageFormatter.format( - context.resources, - mapOf(PLURALS_COUNT to count), - R.string.sharing_videos - ) - } - - override fun getItemsHeadline(count: Int): String { - return PluralsMessageFormatter.format( - context.resources, - mapOf(PLURALS_COUNT to count), - R.string.sharing_items - ) - } -} diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index f2c0564a..5f3bdf40 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -54,7 +54,6 @@ class ImageContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; - private final HeadlineGenerator mHeadlineGenerator; ImageContentPreviewUi( List imageUris, @@ -62,15 +61,13 @@ class ImageContentPreviewUi extends ContentPreviewUi { ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { mImageUris = imageUris; mText = text; mActionFactory = actionFactory; mImageLoader = imageLoader; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; - mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mImageUris); } @@ -83,7 +80,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -116,8 +113,6 @@ class ImageContentPreviewUi extends ContentPreviewUi { imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); imagePreview.setImages(mImageUris, mImageLoader); - updateHeadline(contentPreviewLayout); - return contentPreviewLayout; } @@ -145,17 +140,6 @@ class ImageContentPreviewUi extends ContentPreviewUi { com.android.internal.R.id.content_preview_image_area); } - private void updateHeadline(ViewGroup contentPreview) { - CheckBox includeTextCheckbox = contentPreview.requireViewById(R.id.include_text_action); - if (includeTextCheckbox.getVisibility() == View.VISIBLE - && includeTextCheckbox.isChecked()) { - displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); - } else { - displayHeadline( - contentPreview, mHeadlineGenerator.getImagesHeadline(mImageUris.size())); - } - } - private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) @@ -185,7 +169,6 @@ class ImageContentPreviewUi extends ContentPreviewUi { TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); shareTextAction.accept(!isChecked); - updateHeadline(contentPreview); }); } actionView.setVisibility(visibility); diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index d0cba5bb..e143954e 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -46,7 +46,6 @@ class TextContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final FeatureFlagRepository mFeatureFlagRepository; - private final HeadlineGenerator mHeadlineGenerator; TextContentPreviewUi( @Nullable CharSequence sharingText, @@ -54,15 +53,13 @@ class TextContentPreviewUi extends ContentPreviewUi { @Nullable Uri previewThumbnail, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { mSharingText = sharingText; mPreviewTitle = previewTitle; mPreviewThumbnail = previewThumbnail; mImageLoader = imageLoader; mActionFactory = actionFactory; mFeatureFlagRepository = featureFlagRepository; - mHeadlineGenerator = headlineGenerator; } @Override @@ -73,7 +70,7 @@ class TextContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -125,8 +122,6 @@ class TextContentPreviewUi extends ContentPreviewUi { bitmap)); } - displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText)); - return contentPreviewLayout; } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index b9c64c6f..ee24d18f 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -55,7 +55,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; - private final HeadlineGenerator mHeadlineGenerator; UnifiedContentPreviewUi( List files, @@ -64,8 +63,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { + FeatureFlagRepository featureFlagRepository) { mFiles = files; mText = text; mActionFactory = actionFactory; @@ -73,7 +71,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; - mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mFiles.stream() .map(FileInfo::getPreviewUri) @@ -89,7 +86,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -118,47 +115,20 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return contentPreviewLayout; } + setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); - - List previews = new ArrayList<>(); - boolean allImages = !mFiles.isEmpty(); - boolean allVideos = !mFiles.isEmpty(); - for (FileInfo fileInfo : mFiles) { - ScrollableImagePreviewView.PreviewType previewType = - getPreviewType(fileInfo.getMimeType()); - allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; - allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; - - if (fileInfo.getPreviewUri() != null) { - previews.add(new ScrollableImagePreviewView.Preview( - previewType, - fileInfo.getPreviewUri())); - } - } + List previews = mFiles.stream() + .filter(fileInfo -> fileInfo.getPreviewUri() != null) + .map(fileInfo -> + new ScrollableImagePreviewView.Preview( + getPreviewType(fileInfo.getMimeType()), + fileInfo.getPreviewUri())) + .toList(); imagePreview.setPreviews( previews, mFiles.size() - previews.size(), mImageLoader); - if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) - && !TextUtils.isEmpty(mText) - && mFiles.size() == 1 - && allImages) { - setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); - updateTextWithImageHeadline(contentPreviewLayout); - } else { - if (allImages) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); - } else if (allVideos) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); - } else { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); - } - } - return contentPreviewLayout; } @@ -186,42 +156,38 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { com.android.internal.R.id.content_preview_image_area); } - private void updateTextWithImageHeadline(ViewGroup contentPreview) { - CheckBox actionView = contentPreview.requireViewById(R.id.include_text_action); - if (actionView.getVisibility() == View.VISIBLE && actionView.isChecked()) { - displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); - } else { - displayHeadline( - contentPreview, mHeadlineGenerator.getImagesHeadline(mFiles.size())); - } - } - private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { + int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) + && !TextUtils.isEmpty(mText) + ? View.VISIBLE + : View.GONE; + final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); CheckBox actionView = contentPreview .requireViewById(R.id.include_text_action); - textView.setVisibility(View.VISIBLE); - boolean isLink = HttpUriMatcher.isHttpUri(mText.toString()); + textView.setVisibility(visibility); + boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString()); textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); textView.setText(mText); - final int[] actionLabels = isLink - ? new int[] { R.string.include_link, R.string.exclude_link } - : new int[] { R.string.include_text, R.string.exclude_text }; - final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); - actionView.setChecked(true); - actionView.setText(actionLabels[1]); - shareTextAction.accept(false); - actionView.setOnCheckedChangeListener((view, isChecked) -> { - view.setText(actionLabels[isChecked ? 1 : 0]); - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - shareTextAction.accept(!isChecked); - updateTextWithImageHeadline(contentPreview); - }); - actionView.setVisibility(View.VISIBLE); + if (visibility == View.VISIBLE) { + final int[] actionLabels = isLink + ? new int[] { R.string.include_link, R.string.exclude_link } + : new int[] { R.string.include_text, R.string.exclude_text }; + final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); + actionView.setChecked(true); + actionView.setText(actionLabels[1]); + shareTextAction.accept(false); + actionView.setOnCheckedChangeListener((view, isChecked) -> { + view.setText(actionLabels[isChecked ? 1 : 0]); + TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + shareTextAction.accept(!isChecked); + }); + } + actionView.setVisibility(visibility); } private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 943584a2..596b546e 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -26,7 +26,6 @@ import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; -import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; @@ -99,7 +98,6 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; -import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -1069,43 +1067,6 @@ public class UnbundledChooserActivityTest { .check(matches(isDisplayed())); } - @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) - public void testNoTextPreviewWhenTextIsSharedWithMultipleImages() { - final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); - final String sharedText = "text-" + System.currentTimeMillis(); - - ArrayList uris = new ArrayList<>(); - uris.add(uri); - uris.add(uri); - - Intent sendIntent = createSendUriIntentWithPreview(uris); - sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; - - List resolvedComponentInfos = createResolvedComponentsForTest(2); - - when( - ChooserActivityOverrideData - .getInstance() - .resolverListController - .getResolversForIntentAsUser( - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.anyBoolean(), - Mockito.isA(List.class), - Mockito.any(UserHandle.class))) - .thenReturn(resolvedComponentInfos); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_text)) - .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); - } - @Test public void testOnCreateLogging() { Intent sendIntent = createSendTextIntent(); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 3a00f1f3..04a136b4 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -42,7 +42,6 @@ class ChooserContentPreviewUiTest { private val imageClassifier = MimeTypeClassifier { mimeType -> mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*") } - private val headlineGenerator = mock() private val imageLoader = object : ImageLoader { override fun loadImage(uri: Uri, callback: Consumer) { callback.accept(null) @@ -75,8 +74,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -96,8 +94,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -118,8 +115,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -140,8 +136,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) @@ -164,8 +159,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -194,8 +188,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -224,8 +217,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -256,8 +248,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -286,8 +277,7 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, - headlineGenerator + featureFlagRepository ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt deleted file mode 100644 index 9becce99..00000000 --- a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * 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.contentpreview - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import org.junit.Test -import org.junit.runner.RunWith -import com.google.common.truth.Truth.assertThat - -@RunWith(AndroidJUnit4::class) -class HeadlineGeneratorImplTest { - @Test - fun testHeadlineGeneration() { - val generator = HeadlineGeneratorImpl( - InstrumentationRegistry.getInstrumentation().getTargetContext()) - val str = "Some sting" - val url = "http://www.google.com" - - assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") - assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") - - assertThat(generator.getImageWithTextHeadline(str)).isEqualTo("Sharing image with text") - assertThat(generator.getImageWithTextHeadline(url)).isEqualTo("Sharing image with link") - - assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") - assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") - - assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video") - assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos") - - assertThat(generator.getItemsHeadline(1)).isEqualTo("Sharing 1 item") - assertThat(generator.getItemsHeadline(4)).isEqualTo("Sharing 4 items") - } -} -- cgit v1.2.3-59-g8ed1b From f016e5ff5b3d4d1b515aff469e9e0c18531d0de5 Mon Sep 17 00:00:00 2001 From: Matt Casey Date: Wed, 22 Mar 2023 10:49:49 +0000 Subject: Revert "Revert "Add headline to sharesheet."" This reverts commit 6aaafb277958144a39f9ff4c8328a29eb2db6e38. Reason for revert: Fixing test to submit Change-Id: I99e4f091d5f8812d18415993c07c78e739926c40 --- java/res/layout/chooser_grid_preview_file.xml | 10 +-- java/res/layout/chooser_grid_preview_image.xml | 16 +--- java/res/layout/chooser_grid_preview_text.xml | 10 +-- java/res/layout/chooser_headline_row.xml | 57 ++++++++++++ java/res/values/strings.xml | 30 +++++++ .../android/intentresolver/ChooserActivity.java | 4 +- .../contentpreview/ChooserContentPreviewUi.java | 42 ++++++--- .../contentpreview/ContentPreviewUi.java | 17 +++- .../contentpreview/FileContentPreviewUi.java | 9 +- .../contentpreview/HeadlineGenerator.kt | 35 ++++++++ .../contentpreview/HeadlineGeneratorImpl.kt | 67 ++++++++++++++ .../contentpreview/ImageContentPreviewUi.java | 21 ++++- .../contentpreview/TextContentPreviewUi.java | 9 +- .../contentpreview/UnifiedContentPreviewUi.java | 100 ++++++++++++++------- .../UnbundledChooserActivityTest.java | 39 ++++++++ .../contentpreview/ChooserContentPreviewUiTest.kt | 34 ++++--- .../contentpreview/HeadlineGeneratorImplTest.kt | 49 ++++++++++ 17 files changed, 454 insertions(+), 95 deletions(-) create mode 100644 java/res/layout/chooser_headline_row.xml create mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt create mode 100644 java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 6ba06b3d..036c5318 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -27,6 +27,8 @@ android:paddingBottom="@dimen/chooser_view_spacing" android:background="?android:attr/colorBackground"> + + - - - + - - + + - - + + + + + + + + + + + diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index d1c97c7e..f38666e4 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -137,6 +137,36 @@ } + + Sharing text + + Sharing link + + {count, plural, + =1 {Sharing image} + other {Sharing # images} + } + + + {count, plural, + =1 {Sharing video} + other {Sharing # videos} + } + + + {count, plural, + =1 {Sharing # item} + other {Sharing # items} + } + + + Sharing image with text + + Sharing image with link + No recommended people to share with diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 341e1d52..1dce3b97 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -84,6 +84,7 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; +import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.flags.Flags; @@ -291,7 +292,8 @@ public class ChooserActivity extends ResolverActivity implements createPreviewImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, - mFeatureFlagRepository); + mFeatureFlagRepository, + new HeadlineGeneratorImpl(this)); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index de454cfd..6892b32c 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -103,7 +103,8 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mContentPreviewUi = createContentPreview( targetIntent, @@ -112,7 +113,8 @@ public final class ChooserContentPreviewUi { imageLoader, actionFactory, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { transitionElementStatusCallback.onAllTransitionElementsReady(); } @@ -125,7 +127,8 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { /* In {@link android.content.Intent#getType}, the app may specify a very general mime type * that broadly covers all data being shared, such as {@literal *}/* when sending an image @@ -139,12 +142,20 @@ public final class ChooserContentPreviewUi { if (!(isSend || isSendMultiple) || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) { return createTextPreview( - targetIntent, actionFactory, imageLoader, featureFlagRepository); + targetIntent, + actionFactory, + imageLoader, + featureFlagRepository, + headlineGenerator); } List uris = extractContentUris(targetIntent); if (uris.isEmpty()) { return createTextPreview( - targetIntent, actionFactory, imageLoader, featureFlagRepository); + targetIntent, + actionFactory, + imageLoader, + featureFlagRepository, + headlineGenerator); } ArrayList files = new ArrayList<>(uris.size()); int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); @@ -153,7 +164,8 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { return new UnifiedContentPreviewUi( @@ -163,14 +175,16 @@ public final class ChooserContentPreviewUi { imageLoader, typeClassifier, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } if (previewCount < uris.size()) { return new FileContentPreviewUi( files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up // here. To preserve the legacy behavior, before using it, check that all uris are images. @@ -180,7 +194,8 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } } return new ImageContentPreviewUi( @@ -192,7 +207,8 @@ public final class ChooserContentPreviewUi { actionFactory, imageLoader, transitionElementStatusCallback, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } public int getPreferredContentPreview() { @@ -307,7 +323,8 @@ public final class ChooserContentPreviewUi { Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); ClipData previewData = targetIntent.getClipData(); @@ -324,7 +341,8 @@ public final class ChooserContentPreviewUi { previewThumbnail, actionFactory, imageLoader, - featureFlagRepository); + featureFlagRepository, + headlineGenerator); } private static List extractContentUris(Intent targetIntent) { diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 79444b4e..a6bc2164 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -24,6 +24,7 @@ import android.content.res.Resources; import android.graphics.Bitmap; import android.net.Uri; import android.os.UserHandle; +import android.text.TextUtils; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -114,7 +115,21 @@ abstract class ContentPreviewUi { fadeAnim.start(); } - protected static void displayPayloadReselectionAction( + protected static void displayHeadline(ViewGroup layout, String headline) { + if (layout != null) { + TextView titleView = layout.findViewById(R.id.headline); + if (titleView != null) { + if (!TextUtils.isEmpty(headline)) { + titleView.setText(headline); + titleView.setVisibility(View.VISIBLE); + } else { + titleView.setVisibility(View.GONE); + } + } + } + } + + protected static void displayModifyShareAction( ViewGroup layout, ChooserContentPreviewUi.ActionFactory actionFactory, FeatureFlagRepository featureFlagRepository) { diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 2c5def8b..52e20cf0 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -45,16 +45,19 @@ class FileContentPreviewUi extends ContentPreviewUi { private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; FileContentPreviewUi( List files, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mFiles = files; mActionFactory = actionFactory; mImageLoader = imageLoader; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; } @Override @@ -65,7 +68,7 @@ class FileContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(resources, layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -77,6 +80,8 @@ class FileContentPreviewUi extends ContentPreviewUi { final int uriCount = mFiles.size(); + displayHeadline(contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); + if (uriCount == 0) { contentPreviewLayout.setVisibility(View.GONE); Log.i(TAG, "Appears to be no uris available in EXTRA_STREAM," diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt new file mode 100644 index 00000000..e32bb5c4 --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt @@ -0,0 +1,35 @@ +/* + * 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.contentpreview + +private const val PLURALS_COUNT = "count" + +/** + * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief + * description of the content being shared. + */ +interface HeadlineGenerator { + fun getTextHeadline(text: CharSequence): String + + fun getImageWithTextHeadline(text: CharSequence): String + + fun getImagesHeadline(count: Int): String + + fun getVideosHeadline(count: Int): String + + fun getItemsHeadline(count: Int): String +} diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt new file mode 100644 index 00000000..ae44294c --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -0,0 +1,67 @@ +/* + * 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.contentpreview + +import android.content.Context +import com.android.intentresolver.R +import android.util.PluralsMessageFormatter + +private const val PLURALS_COUNT = "count" + +/** + * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief + * description of the content being shared. + */ +class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator { + override fun getTextHeadline(text: CharSequence): String { + if (text.toString().isHttpUri()) { + return context.getString(R.string.sharing_link) + } + return context.getString(R.string.sharing_text) + } + + override fun getImageWithTextHeadline(text: CharSequence): String { + if (text.toString().isHttpUri()) { + return context.getString(R.string.sharing_image_with_link) + } + return context.getString(R.string.sharing_image_with_text) + } + + override fun getImagesHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_images + ) + } + + override fun getVideosHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_videos + ) + } + + override fun getItemsHeadline(count: Int): String { + return PluralsMessageFormatter.format( + context.resources, + mapOf(PLURALS_COUNT to count), + R.string.sharing_items + ) + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index 5f3bdf40..f2c0564a 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -54,6 +54,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; ImageContentPreviewUi( List imageUris, @@ -61,13 +62,15 @@ class ImageContentPreviewUi extends ContentPreviewUi { ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mImageUris = imageUris; mText = text; mActionFactory = actionFactory; mImageLoader = imageLoader; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mImageUris); } @@ -80,7 +83,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -113,6 +116,8 @@ class ImageContentPreviewUi extends ContentPreviewUi { imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); imagePreview.setImages(mImageUris, mImageLoader); + updateHeadline(contentPreviewLayout); + return contentPreviewLayout; } @@ -140,6 +145,17 @@ class ImageContentPreviewUi extends ContentPreviewUi { com.android.internal.R.id.content_preview_image_area); } + private void updateHeadline(ViewGroup contentPreview) { + CheckBox includeTextCheckbox = contentPreview.requireViewById(R.id.include_text_action); + if (includeTextCheckbox.getVisibility() == View.VISIBLE + && includeTextCheckbox.isChecked()) { + displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); + } else { + displayHeadline( + contentPreview, mHeadlineGenerator.getImagesHeadline(mImageUris.size())); + } + } + private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) @@ -169,6 +185,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); shareTextAction.accept(!isChecked); + updateHeadline(contentPreview); }); } actionView.setVisibility(visibility); diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index e143954e..d0cba5bb 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -46,6 +46,7 @@ class TextContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; TextContentPreviewUi( @Nullable CharSequence sharingText, @@ -53,13 +54,15 @@ class TextContentPreviewUi extends ContentPreviewUi { @Nullable Uri previewThumbnail, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mSharingText = sharingText; mPreviewTitle = previewTitle; mPreviewThumbnail = previewThumbnail; mImageLoader = imageLoader; mActionFactory = actionFactory; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; } @Override @@ -70,7 +73,7 @@ class TextContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -122,6 +125,8 @@ class TextContentPreviewUi extends ContentPreviewUi { bitmap)); } + displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText)); + return contentPreviewLayout; } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index c4e6feb7..2d2ae52b 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -55,6 +55,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; private final FeatureFlagRepository mFeatureFlagRepository; + private final HeadlineGenerator mHeadlineGenerator; UnifiedContentPreviewUi( List files, @@ -63,7 +64,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository) { + FeatureFlagRepository featureFlagRepository, + HeadlineGenerator headlineGenerator) { mFiles = files; mText = text; mActionFactory = actionFactory; @@ -71,6 +73,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; mFeatureFlagRepository = featureFlagRepository; + mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mFiles.stream() .map(FileInfo::getPreviewUri) @@ -86,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); return layout; } @@ -115,20 +118,47 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return contentPreviewLayout; } - setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); - List previews = mFiles.stream() - .filter(fileInfo -> fileInfo.getPreviewUri() != null) - .map(fileInfo -> - new ScrollableImagePreviewView.Preview( - getPreviewType(fileInfo.getMimeType()), - fileInfo.getPreviewUri())) - .toList(); + + List previews = new ArrayList<>(); + boolean allImages = !mFiles.isEmpty(); + boolean allVideos = !mFiles.isEmpty(); + for (FileInfo fileInfo : mFiles) { + ScrollableImagePreviewView.PreviewType previewType = + getPreviewType(fileInfo.getMimeType()); + allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; + allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; + + if (fileInfo.getPreviewUri() != null) { + previews.add(new ScrollableImagePreviewView.Preview( + previewType, + fileInfo.getPreviewUri())); + } + } imagePreview.setPreviews( previews, mFiles.size() - previews.size(), mImageLoader); + if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) + && !TextUtils.isEmpty(mText) + && mFiles.size() == 1 + && allImages) { + setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); + updateTextWithImageHeadline(contentPreviewLayout); + } else { + if (allImages) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + } else if (allVideos) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); + } else { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); + } + } + return contentPreviewLayout; } @@ -155,38 +185,42 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return previewLayout.findViewById(R.id.scrollable_image_preview); } + private void updateTextWithImageHeadline(ViewGroup contentPreview) { + CheckBox actionView = contentPreview.requireViewById(R.id.include_text_action); + if (actionView.getVisibility() == View.VISIBLE && actionView.isChecked()) { + displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); + } else { + displayHeadline( + contentPreview, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + } + } + private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { - int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) - && !TextUtils.isEmpty(mText) - ? View.VISIBLE - : View.GONE; - final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); CheckBox actionView = contentPreview .requireViewById(R.id.include_text_action); - textView.setVisibility(visibility); - boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString()); + textView.setVisibility(View.VISIBLE); + boolean isLink = HttpUriMatcher.isHttpUri(mText.toString()); textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); textView.setText(mText); - if (visibility == View.VISIBLE) { - final int[] actionLabels = isLink - ? new int[] { R.string.include_link, R.string.exclude_link } - : new int[] { R.string.include_text, R.string.exclude_text }; - final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); - actionView.setChecked(true); - actionView.setText(actionLabels[1]); - shareTextAction.accept(false); - actionView.setOnCheckedChangeListener((view, isChecked) -> { - view.setText(actionLabels[isChecked ? 1 : 0]); - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - shareTextAction.accept(!isChecked); - }); - } - actionView.setVisibility(visibility); + final int[] actionLabels = isLink + ? new int[] { R.string.include_link, R.string.exclude_link } + : new int[] { R.string.include_text, R.string.exclude_text }; + final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); + actionView.setChecked(true); + actionView.setText(actionLabels[1]); + shareTextAction.accept(false); + actionView.setOnCheckedChangeListener((view, isChecked) -> { + view.setText(actionLabels[isChecked ? 1 : 0]); + TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + shareTextAction.accept(!isChecked); + updateTextWithImageHeadline(contentPreview); + }); + actionView.setVisibility(View.VISIBLE); } private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 8b9aaa60..9139a4c6 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -26,6 +26,7 @@ import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; @@ -98,6 +99,7 @@ import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.test.espresso.contrib.RecyclerViewActions; import androidx.test.espresso.matcher.BoundedDiagnosingMatcher; +import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; @@ -1067,6 +1069,43 @@ public class UnbundledChooserActivityTest { .check(matches(isDisplayed())); } + @Test + @RequireFeatureFlags( + flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, + values = { true }) + public void testNoTextPreviewWhenTextIsSharedWithMultipleImages() { + final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + final String sharedText = "text-" + System.currentTimeMillis(); + + ArrayList uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); + ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().isImageType = true; + + List resolvedComponentInfos = createResolvedComponentsForTest(2); + + when( + ChooserActivityOverrideData + .getInstance() + .resolverListController + .getResolversForIntentAsUser( + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.anyBoolean(), + Mockito.isA(List.class), + Mockito.any(UserHandle.class))) + .thenReturn(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(com.android.internal.R.id.content_preview_text)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + } + @Test public void testOnCreateLogging() { Intent sendIntent = createSendTextIntent(); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 58b8a21d..82bf94c4 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -44,6 +44,7 @@ class ChooserContentPreviewUiTest { private val imageClassifier = MimeTypeClassifier { mimeType -> mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*") } + private val headlineGenerator = mock() private val imageLoader = object : ImageLoader { override fun loadImage(uri: Uri, callback: Consumer) { callback.accept(null) @@ -76,7 +77,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -96,7 +98,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -117,7 +120,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -138,7 +142,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) @@ -160,7 +165,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) @@ -185,7 +191,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) @@ -208,7 +215,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -237,7 +245,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -266,7 +275,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -297,7 +307,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -326,7 +337,8 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository + featureFlagRepository, + headlineGenerator ) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt new file mode 100644 index 00000000..9becce99 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt @@ -0,0 +1,49 @@ +/* + * 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.contentpreview + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Test +import org.junit.runner.RunWith +import com.google.common.truth.Truth.assertThat + +@RunWith(AndroidJUnit4::class) +class HeadlineGeneratorImplTest { + @Test + fun testHeadlineGeneration() { + val generator = HeadlineGeneratorImpl( + InstrumentationRegistry.getInstrumentation().getTargetContext()) + val str = "Some sting" + val url = "http://www.google.com" + + assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") + assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") + + assertThat(generator.getImageWithTextHeadline(str)).isEqualTo("Sharing image with text") + assertThat(generator.getImageWithTextHeadline(url)).isEqualTo("Sharing image with link") + + assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") + assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") + + assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video") + assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos") + + assertThat(generator.getItemsHeadline(1)).isEqualTo("Sharing 1 item") + assertThat(generator.getItemsHeadline(4)).isEqualTo("Sharing 4 items") + } +} -- cgit v1.2.3-59-g8ed1b From 10f22425705485803cc723bcdc61623926a402c9 Mon Sep 17 00:00:00 2001 From: Himanshu Gupta Date: Sat, 25 Feb 2023 14:39:01 +0000 Subject: Migrating AppCloning code to unbundled sharesheet. This CL encapsulates the work done in previous CLs for the frameworks sharesheet: 1. ag/20982903 2. ag/20980382 3. ag/20480246 4. ag/21072117 5. ag/21223364 6. ag/21117396 7. ag/21465730 With this CL, Cloned Apps can be shown in unbundled sharesheet, at par with frameworks. Bug: 273294251 Test: atest com.android.intentresolver Change-Id: Ic01a93f7279c8beb998b3e98f53c459c5ed2b1bf --- AndroidManifest.xml | 1 + .../AbstractMultiProfilePagerAdapter.java | 9 +- .../intentresolver/AnnotatedUserHandles.java | 42 ++- .../android/intentresolver/ChooserActivity.java | 85 +++-- .../android/intentresolver/ChooserListAdapter.java | 9 +- .../ChooserMultiProfilePagerAdapter.java | 6 + .../GenericMultiProfilePagerAdapter.java | 24 +- .../NoAppsAvailableEmptyStateProvider.java | 12 +- .../NoCrossProfileEmptyStateProvider.java | 11 +- .../android/intentresolver/ResolverActivity.java | 184 +++++++--- .../intentresolver/ResolverListAdapter.java | 33 +- .../intentresolver/ResolverListController.java | 45 +-- .../ResolverMultiProfilePagerAdapter.java | 10 +- .../model/AbstractResolverComparator.java | 72 +++- .../AppPredictionServiceResolverComparator.java | 43 ++- .../model/ResolverComparatorModel.java | 7 +- .../ResolverRankerServiceResolverComparator.java | 281 +++++++++------ .../ChooserActivityOverrideData.java | 16 +- .../intentresolver/ChooserListAdapterTest.kt | 9 +- .../intentresolver/ChooserWrapperActivity.java | 32 +- .../intentresolver/ResolverActivityTest.java | 397 +++++++++++++++++---- .../intentresolver/ResolverDataProvider.java | 48 +++ .../intentresolver/ResolverWrapperActivity.java | 50 +-- .../intentresolver/ResolverWrapperAdapter.java | 6 +- .../intentresolver/ShortcutSelectionLogicTest.kt | 9 +- .../UnbundledChooserActivityTest.java | 142 ++++++-- .../UnbundledChooserActivityWorkProfileTest.java | 16 +- .../chooser/ImmutableTargetInfoTest.kt | 12 +- .../intentresolver/chooser/TargetInfoTest.kt | 9 +- .../model/AbstractResolverComparatorTest.java | 8 +- 30 files changed, 1186 insertions(+), 442 deletions(-) (limited to 'java/src') diff --git a/AndroidManifest.xml b/AndroidManifest.xml index fc99c0b2..5228827d 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -35,6 +35,7 @@ + mLoadedPages; private final EmptyStateProvider mEmptyStateProvider; private final UserHandle mWorkProfileUserHandle; + private final UserHandle mCloneProfileUserHandle; private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. AbstractMultiProfilePagerAdapter( @@ -69,11 +70,13 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { int currentPage, EmptyStateProvider emptyStateProvider, Supplier workProfileQuietModeChecker, - UserHandle workProfileUserHandle) { + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle) { mContext = Objects.requireNonNull(context); mCurrentPage = currentPage; mLoadedPages = new HashSet<>(); mWorkProfileUserHandle = workProfileUserHandle; + mCloneProfileUserHandle = cloneProfileUserHandle; mEmptyStateProvider = emptyStateProvider; mWorkProfileQuietModeChecker = workProfileQuietModeChecker; } @@ -160,6 +163,10 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter { return null; } + public UserHandle getCloneUserHandle() { + return mCloneProfileUserHandle; + } + /** * Returns the {@link ProfileDescriptor} relevant to the given pageIndex. *

    diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java index b4365b84..769195ed 100644 --- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java +++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java @@ -87,6 +87,11 @@ public final class AnnotatedUserHandles { // TODO: integrate logic for `ResolverActivity.EXTRA_CALLING_USER`. userHandleSharesheetLaunchedAs = UserHandle.of(UserHandle.myUserId()); + // ActivityManager.getCurrentUser() refers to the current Foreground user. When clone/work + // profile is active, we always make the personal tab from the foreground user. + // Outside profiles, current foreground user is potentially the same as the sharesheet + // process's user (UserHandle.myUserId()), so we continue to create personal tab with the + // current foreground user. personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser()); UserManager userManager = forShareActivity.getSystemService(UserManager.class); @@ -100,14 +105,43 @@ public final class AnnotatedUserHandles { @Nullable private static UserHandle getWorkProfileForUser( UserManager userManager, UserHandle profileOwnerUserHandle) { - return userManager.getProfiles(profileOwnerUserHandle.getIdentifier()).stream() - .filter(info -> info.isManagedProfile()).findFirst() - .map(info -> info.getUserHandle()).orElse(null); + return userManager.getProfiles(profileOwnerUserHandle.getIdentifier()) + .stream() + .filter(info -> info.isManagedProfile()) + .findFirst() + .map(info -> info.getUserHandle()) + .orElse(null); } @Nullable private static UserHandle getCloneProfileForUser( UserManager userManager, UserHandle profileOwnerUserHandle) { - return null; // Not yet supported in framework. + return userManager.getProfiles(profileOwnerUserHandle.getIdentifier()) + .stream() + .filter(info -> info.isCloneProfile()) + .findFirst() + .map(info -> info.getUserHandle()) + .orElse(null); + } + + /** + * Returns the {@link UserHandle} to use when querying resolutions for intents in a + * {@link ResolverListController} configured for the provided {@code userHandle}. + */ + public UserHandle getQueryIntentsUser(UserHandle userHandle) { + // In case launching app is in clonedProfile, and we are building the personal tab, intent + // resolution will be attempted as clonedUser instead of user 0. This is because intent + // resolution from user 0 and clonedUser is not guaranteed to return same results. + // We do not care about the case when personal adapter is started with non-root user + // (secondary user case), as clone profile is guaranteed to be non-active in that case. + UserHandle queryIntentsUser = userHandle; + if (isLaunchedAsCloneProfile() && userHandle.equals(personalProfileUserHandle)) { + queryIntentsUser = cloneProfileUserHandle; + } + return queryIntentsUser; + } + + private Boolean isLaunchedAsCloneProfile() { + return userHandleSharesheetLaunchedAs.equals(cloneProfileUserHandle); } } diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index ae5be26d..bae1feb2 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -495,7 +495,7 @@ public class ChooserActivity extends ResolverActivity implements return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker(), createMyUserIdProvider()); + createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); } private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile( @@ -508,13 +508,14 @@ public class ChooserActivity extends ResolverActivity implements initialIntents, rList, filterLastUsed, - /* userHandle */ UserHandle.of(UserHandle.myUserId())); + /* userHandle */ getPersonalProfileUserHandle()); return new ChooserMultiProfilePagerAdapter( /* context */ this, adapter, createEmptyStateProvider(/* workProfileUserHandle= */ null), /* workProfileQuietModeChecker= */ () -> false, /* workProfileUserHandle= */ null, + getCloneProfileUserHandle(), mMaxTargetsPerRow); } @@ -545,13 +546,14 @@ public class ChooserActivity extends ResolverActivity implements () -> mWorkProfileAvailability.isQuietModeEnabled(), selectedProfile, getWorkProfileUserHandle(), + getCloneProfileUserHandle(), mMaxTargetsPerRow); } private int findSelectedProfile() { int selectedProfile = getSelectedProfileExtra(); if (selectedProfile == -1) { - selectedProfile = getProfileForUser(getUser()); + selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch()); } return selectedProfile; } @@ -860,7 +862,11 @@ public class ChooserActivity extends ResolverActivity implements ChooserTargetActionsDialogFragment.show( getSupportFragmentManager(), targetList, - mChooserMultiProfilePagerAdapter.getCurrentUserHandle(), + // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be + // resolved correctly within the same tab. + getResolveInfoUserHandle( + targetInfo.getResolveInfo(), + mChooserMultiProfilePagerAdapter.getCurrentUserHandle()), shortcutIdKey, shortcutTitle, isShortcutPinned, @@ -892,11 +898,14 @@ public class ChooserActivity extends ResolverActivity implements if (targetInfo.isMultiDisplayResolveInfo()) { MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo; if (!mti.hasSelected()) { + // Add userHandle based badge to the stackedAppDialogBox. ChooserStackedAppDialogFragment.show( getSupportFragmentManager(), mti, which, - mChooserMultiProfilePagerAdapter.getCurrentUserHandle()); + getResolveInfoUserHandle( + targetInfo.getResolveInfo(), + mChooserMultiProfilePagerAdapter.getCurrentUserHandle())); return; } } @@ -1008,9 +1017,11 @@ public class ChooserActivity extends ResolverActivity implements mChooserMultiProfilePagerAdapter.getActiveListAdapter(); if (currentListAdapter != null) { sendImpressionToAppPredictor(info, currentListAdapter); - currentListAdapter.updateModel(info.getResolvedComponentName()); - currentListAdapter.updateChooserCounts(ri.activityInfo.packageName, - targetIntent.getAction()); + currentListAdapter.updateModel(info); + currentListAdapter.updateChooserCounts( + ri.activityInfo.packageName, + targetIntent.getAction(), + ri.userHandle); } if (DEBUG) { Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName); @@ -1096,22 +1107,33 @@ public class ChooserActivity extends ResolverActivity implements @Nullable private AppPredictor getAppPredictor(UserHandle userHandle) { ProfileRecord record = getProfileRecord(userHandle); - return (record == null) ? null : record.appPredictor; + // We cannot use APS service when clone profile is present as APS service cannot sort + // cross profile targets as of now. + return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor; } /** * Sort intents alphabetically based on display label. */ static class AzInfoComparator implements Comparator { - Collator mCollator; + Comparator mComparator; AzInfoComparator(Context context) { - mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); + Collator collator = Collator + .getInstance(context.getResources().getConfiguration().locale); + // Adding two stage comparator, first stage compares using displayLabel, next stage + // compares using resolveInfo.userHandle + mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator) + .thenComparingInt(displayResolveInfo -> + getResolveInfoUserHandle( + displayResolveInfo.getResolveInfo(), + // TODO: User resolveInfo.userHandle, once its available. + UserHandle.SYSTEM).getIdentifier()); } @Override public int compare( DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) { - return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel()); + return mComparator.compare(lhsp, rhsp); } } @@ -1129,14 +1151,16 @@ public class ChooserActivity extends ResolverActivity implements Intent targetIntent, String referrerPackageName, int launchedFromUid, - AbstractResolverComparator resolverComparator) { + AbstractResolverComparator resolverComparator, + UserHandle queryIntentsAsUser) { super( context, pm, targetIntent, referrerPackageName, launchedFromUid, - resolverComparator); + resolverComparator, + queryIntentsAsUser); } @Override @@ -1255,20 +1279,24 @@ public class ChooserActivity extends ResolverActivity implements Intent targetIntent, ChooserRequestParameters chooserRequest, int maxTargetsPerRow) { + UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() + && userHandle.equals(getPersonalProfileUserHandle()) + ? getCloneProfileUserHandle() : userHandle; return new ChooserListAdapter( context, payloadIntents, initialIntents, rList, filterLastUsed, - resolverListController, + createListController(userHandle), userHandle, targetIntent, this, context.getPackageManager(), getChooserActivityLogger(), chooserRequest, - maxTargetsPerRow); + maxTargetsPerRow, + initialIntentsUserSpace); } @Override @@ -1281,8 +1309,13 @@ public class ChooserActivity extends ResolverActivity implements getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger()); } else { resolverComparator = - new ResolverRankerServiceResolverComparator(this, getTargetIntent(), - getReferrerPackageName(), null, getChooserActivityLogger()); + new ResolverRankerServiceResolverComparator( + this, + getTargetIntent(), + getReferrerPackageName(), + null, + getChooserActivityLogger(), + getResolverRankerServiceUserHandleList(userHandle)); } return new ChooserListController( @@ -1291,7 +1324,8 @@ public class ChooserActivity extends ResolverActivity implements getTargetIntent(), getReferrerPackageName(), getAnnotatedUserHandles().userIdOfCallingApp, - resolverComparator); + resolverComparator, + getQueryIntentsUser(userHandle)); } @VisibleForTesting @@ -1508,17 +1542,16 @@ public class ChooserActivity extends ResolverActivity implements } /** - * Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle - * does not match either the personal or work user handle. + * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle. + * Returns {@link #PROFILE_PERSONAL}, otherwise. **/ private int getProfileForUser(UserHandle currentUserHandle) { - if (currentUserHandle.equals(getPersonalProfileUserHandle())) { - return PROFILE_PERSONAL; - } else if (currentUserHandle.equals(getWorkProfileUserHandle())) { + if (currentUserHandle.equals(getWorkProfileUserHandle())) { return PROFILE_WORK; } - Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile."); - return -1; + // We return personal profile, as it is the default when there is no work profile, personal + // profile represents rootUser, clonedUser & secondaryUser, covering all use cases. + return PROFILE_PERSONAL; } private ViewGroup getActiveEmptyStateView() { diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index f0651360..dab44577 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -142,7 +142,8 @@ public class ChooserListAdapter extends ResolverListAdapter { PackageManager packageManager, ChooserActivityLogger chooserActivityLogger, ChooserRequestParameters chooserRequest, - int maxRankedTargets) { + int maxRankedTargets, + UserHandle initialIntentsUserSpace) { // Don't send the initial intents through the shared ResolverActivity path, // we want to separate them into a different section. super( @@ -155,7 +156,8 @@ public class ChooserListAdapter extends ResolverListAdapter { userHandle, targetIntent, resolverListCommunicator, - false); + false, + initialIntentsUserSpace); mChooserRequest = chooserRequest; mMaxRankedTargets = maxRankedTargets; @@ -222,6 +224,7 @@ public class ChooserListAdapter extends ResolverListAdapter { ri.noResourceId = true; ri.icon = 0; } + ri.userHandle = initialIntentsUserSpace; DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( ii, ri, ii, mPresentationFactory.makePresentationGetter(ri)); mCallerTargets.add(displayResolveInfo); @@ -351,6 +354,8 @@ public class ChooserListAdapter extends ResolverListAdapter { .collect(Collectors.groupingBy(target -> target.getResolvedComponentName().getPackageName() + "#" + target.getDisplayLabel() + + '#' + ResolverActivity.getResolveInfoUserHandle( + target.getResolveInfo(), getUserHandle()).getIdentifier() )) .values() .stream() diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java index 3e2ea473..9c096fd2 100644 --- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java @@ -50,6 +50,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda EmptyStateProvider emptyStateProvider, Supplier workProfileQuietModeChecker, UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, int maxTargetsPerRow) { this( context, @@ -59,6 +60,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda workProfileQuietModeChecker, /* defaultProfile= */ 0, workProfileUserHandle, + cloneProfileUserHandle, new BottomPaddingOverrideSupplier(context)); } @@ -70,6 +72,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda Supplier workProfileQuietModeChecker, @Profile int defaultProfile, UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, int maxTargetsPerRow) { this( context, @@ -79,6 +82,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda workProfileQuietModeChecker, defaultProfile, workProfileUserHandle, + cloneProfileUserHandle, new BottomPaddingOverrideSupplier(context)); } @@ -90,6 +94,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda Supplier workProfileQuietModeChecker, @Profile int defaultProfile, UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { super( context, @@ -100,6 +105,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda workProfileQuietModeChecker, defaultProfile, workProfileUserHandle, + cloneProfileUserHandle, () -> makeProfileView(context), bottomPaddingOverrideSupplier); mAdapterBinder = adapterBinder; diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java index 7613f35f..a1c53402 100644 --- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java @@ -19,6 +19,7 @@ package com.android.intentresolver; import android.annotation.Nullable; import android.content.Context; import android.os.UserHandle; +import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -84,6 +85,7 @@ class GenericMultiProfilePagerAdapter< Supplier workProfileQuietModeChecker, @Profile int defaultProfile, UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, Supplier pageViewInflater, Supplier> containerBottomPaddingOverrideSupplier) { super( @@ -91,7 +93,8 @@ class GenericMultiProfilePagerAdapter< /* currentPage= */ defaultProfile, emptyStateProvider, workProfileQuietModeChecker, - workProfileUserHandle); + workProfileUserHandle, + cloneProfileUserHandle); mListAdapterExtractor = listAdapterExtractor; mAdapterBinder = adapterBinder; @@ -145,12 +148,12 @@ class GenericMultiProfilePagerAdapter< @Override @Nullable protected ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { - if (getActiveListAdapter().getUserHandle().equals(userHandle)) { - return getActiveListAdapter(); - } - if ((getInactiveListAdapter() != null) && getInactiveListAdapter().getUserHandle().equals( - userHandle)) { - return getInactiveListAdapter(); + if (getPersonalListAdapter().getUserHandle().equals(userHandle) + || userHandle.equals(getCloneUserHandle())) { + return getPersonalListAdapter(); + } else if (getWorkListAdapter() != null + && getWorkListAdapter().getUserHandle().equals(userHandle)) { + return getWorkListAdapter(); } return null; } @@ -177,6 +180,9 @@ class GenericMultiProfilePagerAdapter< @Override public ListAdapterT getWorkListAdapter() { + if (!hasAdapterForIndex(PROFILE_WORK)) { + return null; + } return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK)); } @@ -209,6 +215,10 @@ class GenericMultiProfilePagerAdapter< paddingBottom)); } + private boolean hasAdapterForIndex(int pageIndex) { + return (pageIndex < getCount()); + } + // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager" // should be the owner of all per-profile data (especially now that the API is generic)? private static class GenericProfileDescriptor extends diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java index d424f295..a7b50f38 100644 --- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java @@ -30,7 +30,7 @@ import android.stats.devicepolicy.nano.DevicePolicyEnums; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; +import com.android.internal.R; import java.util.List; @@ -49,16 +49,16 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { @NonNull private final String mMetricsCategory; @NonNull - private final MyUserIdProvider mMyUserIdProvider; + private final UserHandle mTabOwnerUserHandleForLaunch; public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle, UserHandle personalProfileUserHandle, String metricsCategory, - MyUserIdProvider myUserIdProvider) { + UserHandle tabOwnerUserHandleForLaunch) { mContext = context; mWorkProfileUserHandle = workProfileUserHandle; mPersonalProfileUserHandle = personalProfileUserHandle; mMetricsCategory = metricsCategory; - mMyUserIdProvider = myUserIdProvider; + mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; } @Nullable @@ -68,7 +68,7 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { UserHandle listUserHandle = resolverListAdapter.getUserHandle(); if (mWorkProfileUserHandle != null - && (mMyUserIdProvider.getMyUserId() == listUserHandle.getIdentifier() + && (mTabOwnerUserHandleForLaunch.equals(listUserHandle) || !hasAppsInOtherProfile(resolverListAdapter))) { String title; @@ -101,7 +101,7 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { return false; } List resolversForIntent = - adapter.getResolversForUser(UserHandle.of(mMyUserIdProvider.getMyUserId())); + adapter.getResolversForUser(mTabOwnerUserHandleForLaunch); for (ResolvedComponentInfo info : resolversForIntent) { ResolveInfo resolveInfo = info.getResolveInfoAt(0); if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) { diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java index 420d26c5..6f72bb00 100644 --- a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java @@ -27,7 +27,6 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; /** * Empty state provider that does not allow cross profile sharing, it will return a blocker @@ -39,28 +38,28 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { private final EmptyState mNoWorkToPersonalEmptyState; private final EmptyState mNoPersonalToWorkEmptyState; private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; - private final MyUserIdProvider mUserIdProvider; + private final UserHandle mTabOwnerUserHandleForLaunch; public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle, EmptyState noWorkToPersonalEmptyState, EmptyState noPersonalToWorkEmptyState, CrossProfileIntentsChecker crossProfileIntentsChecker, - MyUserIdProvider myUserIdProvider) { + UserHandle tabOwnerUserHandleForLaunch) { mPersonalProfileUserHandle = personalUserHandle; mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; mCrossProfileIntentsChecker = crossProfileIntentsChecker; - mUserIdProvider = myUserIdProvider; + mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; } @Nullable @Override public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { boolean shouldShowBlocker = - mUserIdProvider.getMyUserId() != resolverListAdapter.getUserHandle().getIdentifier() + !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle()) && !mCrossProfileIntentsChecker .hasCrossProfileIntents(resolverListAdapter.getIntents(), - mUserIdProvider.getMyUserId(), + mTabOwnerUserHandleForLaunch.getIdentifier(), resolverListAdapter.getUserHandle().getIdentifier()); if (!shouldShowBlocker) { diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index a240968b..3b9d2a53 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -33,6 +33,8 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK; import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; + import android.annotation.Nullable; import android.annotation.StringRes; import android.annotation.UiThread; @@ -106,6 +108,7 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile; import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; @@ -375,8 +378,11 @@ public class ResolverActivity extends FragmentActivity implements // turn this off when running under voice interaction, since it results in // a more complicated UI that the current voice interaction flow is not able // to handle. We also turn it off when the work tab is shown to simplify the UX. + // We also turn it off when clonedProfile is present on the device, because we might have + // different "last chosen" activities in the different profiles, and PackageManager doesn't + // provide any more information to help us select between them. boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction() - && !shouldShowTabs(); + && !shouldShowTabs() && !hasCloneProfile(); mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed); if (configureContentView()) { return; @@ -480,7 +486,7 @@ public class ResolverActivity extends FragmentActivity implements return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(), noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker(), createMyUserIdProvider()); + createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch()); } protected int appliedThemeResId() { @@ -857,13 +863,23 @@ public class ResolverActivity extends FragmentActivity implements // the future if resolver *were* to make any (non-overridden) calls to a version that used a // different signature (and thus didn't return the subclass type). @VisibleForTesting - protected ResolverListController createListController(UserHandle unused) { + protected ResolverListController createListController(UserHandle userHandle) { + ResolverRankerServiceResolverComparator resolverComparator = + new ResolverRankerServiceResolverComparator( + this, + getTargetIntent(), + getReferrerPackageName(), + null, + null, + getResolverRankerServiceUserHandleList(userHandle)); return new ResolverListController( this, mPm, getTargetIntent(), getReferrerPackageName(), - getAnnotatedUserHandles().userIdOfCallingApp); + getAnnotatedUserHandles().userIdOfCallingApp, + resolverComparator, + getQueryIntentsUser(userHandle)); } /** @@ -1003,27 +1019,6 @@ public class ResolverActivity extends FragmentActivity implements }); } - // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. - // @NonFinalForTesting - @Nullable - protected UserHandle getWorkProfileUserHandle() { - return getAnnotatedUserHandles().workProfileUserHandle; - } - - // @NonFinalForTesting - @VisibleForTesting - public void safelyStartActivity(TargetInfo cti) { - // We're dispatching intents that might be coming from legacy apps, so - // don't kill ourselves. - StrictMode.disableDeathOnFileUriExposure(); - try { - UserHandle currentUserHandle = mMultiProfilePagerAdapter.getCurrentUserHandle(); - safelyStartActivityInternal(cti, currentUserHandle, null); - } finally { - StrictMode.enableDeathOnFileUriExposure(); - } - } - // @NonFinalForTesting @VisibleForTesting protected ResolverListAdapter createResolverListAdapter(Context context, @@ -1032,6 +1027,9 @@ public class ResolverActivity extends FragmentActivity implements Intent startIntent = getIntent(); boolean isAudioCaptureDevice = startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false); + UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile() + && userHandle.equals(getPersonalProfileUserHandle()) + ? getCloneProfileUserHandle() : userHandle; return new ResolverListAdapter( context, payloadIntents, @@ -1042,7 +1040,8 @@ public class ResolverActivity extends FragmentActivity implements userHandle, getTargetIntent(), this, - isAudioCaptureDevice); + isAudioCaptureDevice, + initialIntentsUserSpace); } private LatencyTracker getLatencyTracker() { @@ -1081,7 +1080,7 @@ public class ResolverActivity extends FragmentActivity implements workProfileUserHandle, getPersonalProfileUserHandle(), getMetricsCategory(), - createMyUserIdProvider() + getTabOwnerUserHandleForLaunch() ); // Return composite provider, the order matters (the higher, the more priority) @@ -1124,19 +1123,20 @@ public class ResolverActivity extends FragmentActivity implements initialIntents, rList, filterLastUsed, - /* userHandle */ UserHandle.of(UserHandle.myUserId())); + /* userHandle */ getPersonalProfileUserHandle()); return new ResolverMultiProfilePagerAdapter( /* context */ this, adapter, createEmptyStateProvider(/* workProfileUserHandle= */ null), /* workProfileQuietModeChecker= */ () -> false, - /* workProfileUserHandle= */ null); + /* workProfileUserHandle= */ null, + getCloneProfileUserHandle()); } private UserHandle getIntentUser() { return getIntent().hasExtra(EXTRA_CALLING_USER) ? getIntent().getParcelableExtra(EXTRA_CALLING_USER) - : getUser(); + : getTabOwnerUserHandleForLaunch(); } private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( @@ -1148,7 +1148,7 @@ public class ResolverActivity extends FragmentActivity implements // this happens, we check for it here and set the current profile's tab. int selectedProfile = getCurrentProfile(); UserHandle intentUser = getIntentUser(); - if (!getUser().equals(intentUser)) { + if (!getTabOwnerUserHandleForLaunch().equals(intentUser)) { if (getPersonalProfileUserHandle().equals(intentUser)) { selectedProfile = PROFILE_PERSONAL; } else if (getWorkProfileUserHandle().equals(intentUser)) { @@ -1187,7 +1187,8 @@ public class ResolverActivity extends FragmentActivity implements createEmptyStateProvider(getWorkProfileUserHandle()), () -> mWorkProfileAvailability.isQuietModeEnabled(), selectedProfile, - getWorkProfileUserHandle()); + getWorkProfileUserHandle(), + getCloneProfileUserHandle()); } /** @@ -1210,7 +1211,8 @@ public class ResolverActivity extends FragmentActivity implements } protected final @Profile int getCurrentProfile() { - return (UserHandle.myUserId() == UserHandle.USER_SYSTEM ? PROFILE_PERSONAL : PROFILE_WORK); + return (getTabOwnerUserHandleForLaunch().equals(getPersonalProfileUserHandle()) + ? PROFILE_PERSONAL : PROFILE_WORK); } protected final AnnotatedUserHandles getAnnotatedUserHandles() { @@ -1221,10 +1223,43 @@ public class ResolverActivity extends FragmentActivity implements return getAnnotatedUserHandles().personalProfileUserHandle; } + // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. + // @NonFinalForTesting + @Nullable + protected UserHandle getWorkProfileUserHandle() { + return getAnnotatedUserHandles().workProfileUserHandle; + } + + // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. + @Nullable + protected UserHandle getCloneProfileUserHandle() { + return getAnnotatedUserHandles().cloneProfileUserHandle; + } + + // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`. + protected UserHandle getTabOwnerUserHandleForLaunch() { + return getAnnotatedUserHandles().tabOwnerUserHandleForLaunch; + } + + protected UserHandle getUserHandleSharesheetLaunchedAs() { + return getAnnotatedUserHandles().userHandleSharesheetLaunchedAs; + } + + private boolean hasWorkProfile() { return getWorkProfileUserHandle() != null; } + private boolean hasCloneProfile() { + return getCloneProfileUserHandle() != null; + } + + protected final boolean isLaunchedAsCloneProfile() { + return hasCloneProfile() + && getUserHandleSharesheetLaunchedAs().equals(getCloneProfileUserHandle()); + } + + protected final boolean shouldShowTabs() { return hasWorkProfile(); } @@ -1498,6 +1533,13 @@ public class ResolverActivity extends FragmentActivity implements mAlwaysButton.setEnabled(false); return; } + // In case of clonedProfile being active, we do not allow the 'Always' option in the + // disambiguation dialog of Personal Profile as the package manager cannot distinguish + // between cross-profile preferred activities. + if (hasCloneProfile() && (mMultiProfilePagerAdapter.getCurrentPage() == PROFILE_PERSONAL)) { + mAlwaysButton.setEnabled(false); + return; + } boolean enabled = false; ResolveInfo ri = null; if (hasValidSelection) { @@ -1572,6 +1614,16 @@ public class ResolverActivity extends FragmentActivity implements } } + /** Start the activity specified by the {@link TargetInfo}.*/ + public final void safelyStartActivity(TargetInfo cti) { + // In case cloned apps are present, we would want to start those apps in cloned user + // space, which will not be same as adaptor's userHandle. resolveInfo.userHandle + // identifies the correct user space in such cases. + UserHandle activityUserHandle = getResolveInfoUserHandle( + cti.getResolveInfo(), mMultiProfilePagerAdapter.getCurrentUserHandle()); + safelyStartActivityAsUser(cti, activityUserHandle, null); + } + /** * Start activity as a fixed user handle. * @param cti TargetInfo to be launched. @@ -1594,7 +1646,8 @@ public class ResolverActivity extends FragmentActivity implements } } - private void safelyStartActivityInternal( + @VisibleForTesting + protected void safelyStartActivityInternal( TargetInfo cti, UserHandle user, @Nullable Bundle options) { // If the target is suspended, the activity will not be successfully launched. // Do not unregister from package manager updates in this case @@ -2172,16 +2225,10 @@ public class ResolverActivity extends FragmentActivity implements public final boolean useLayoutWithDefault() { // We only use the default app layout when the profile of the active user has a // filtered item. We always show the same default app even in the inactive user profile. - boolean currentUserAdapterHasFilteredItem; - if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier() - == UserHandle.myUserId()) { - currentUserAdapterHasFilteredItem = - mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem(); - } else { - currentUserAdapterHasFilteredItem = - mMultiProfilePagerAdapter.getInactiveListAdapter().hasFilteredItem(); - } - return mSupportsAlwaysUseOption && currentUserAdapterHasFilteredItem; + boolean adapterForCurrentUserHasFilteredItem = + mMultiProfilePagerAdapter.getListAdapterForUserHandle( + getTabOwnerUserHandleForLaunch()).hasFilteredItem(); + return mSupportsAlwaysUseOption && adapterForCurrentUserHasFilteredItem; } /** @@ -2200,7 +2247,14 @@ public class ResolverActivity extends FragmentActivity implements return lhs == null ? rhs == null : lhs.activityInfo == null ? rhs.activityInfo == null : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name) - && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName); + && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName) + // Comparing against resolveInfo.userHandle in case cloned apps are present, + // as they will have the same activityInfo. + && Objects.equals( + getResolveInfoUserHandle(lhs, + mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()), + getResolveInfoUserHandle(rhs, + mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle())); } private boolean inactiveListAdapterHasItems() { @@ -2307,4 +2361,44 @@ public class ResolverActivity extends FragmentActivity implements } } } + /** + * Returns the {@link UserHandle} to use when querying resolutions for intents in a + * {@link ResolverListController} configured for the provided {@code userHandle}. + */ + protected final UserHandle getQueryIntentsUser(UserHandle userHandle) { + return mLazyAnnotatedUserHandles.get().getQueryIntentsUser(userHandle); + } + + /** + * Returns the {@link List} of {@link UserHandle} to pass on to the + * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}. + */ + @VisibleForTesting(visibility = PROTECTED) + public final List getResolverRankerServiceUserHandleList(UserHandle userHandle) { + return getResolverRankerServiceUserHandleListInternal(userHandle); + } + + @VisibleForTesting + protected List getResolverRankerServiceUserHandleListInternal( + UserHandle userHandle) { + List userList = new ArrayList<>(); + userList.add(userHandle); + // Add clonedProfileUserHandle to the list only if we are: + // a. Building the Personal Tab. + // b. CloneProfile exists on the device. + if (userHandle.equals(getPersonalProfileUserHandle()) + && getCloneProfileUserHandle() != null) { + userList.add(getCloneProfileUserHandle()); + } + return userList; + } + + /** + * This function is temporary in nature, and its usages will be replaced with just + * resolveInfo.userHandle, once it is available, once sharesheet is stable. + */ + public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo, + UserHandle predictedHandle) { + return resolveInfo.userHandle; + } } diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index eac275cc..b0586f2d 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -97,6 +97,8 @@ public class ResolverListAdapter extends BaseAdapter { private boolean mFilterLastUsed; private Runnable mPostListReadyRunnable; private boolean mIsTabLoaded; + // Represents the UserSpace in which the Initial Intents should be resolved. + private final UserHandle mInitialIntentsUserSpace; public ResolverListAdapter( Context context, @@ -108,7 +110,8 @@ public class ResolverListAdapter extends BaseAdapter { UserHandle userHandle, Intent targetIntent, ResolverListCommunicator resolverListCommunicator, - boolean isAudioCaptureDevice) { + boolean isAudioCaptureDevice, + UserHandle initialIntentsUserSpace) { mContext = context; mIntents = payloadIntents; mInitialIntents = initialIntents; @@ -125,6 +128,7 @@ public class ResolverListAdapter extends BaseAdapter { final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE); mIconDpi = am.getLauncherLargeIconDensity(); mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi); + mInitialIntentsUserSpace = initialIntentsUserSpace; } public final DisplayResolveInfo getFirstDisplayResolveInfo() { @@ -176,19 +180,25 @@ public class ResolverListAdapter extends BaseAdapter { } /** - * Returns the app share score of the given {@code componentName}. + * Returns the app share score of the given {@code targetInfo}. */ - public float getScore(ComponentName componentName) { - return mResolverListController.getScore(componentName); + public float getScore(TargetInfo targetInfo) { + return mResolverListController.getScore(targetInfo); } - public void updateModel(ComponentName componentName) { - mResolverListController.updateModel(componentName); + /** + * Updates the model about the chosen {@code targetInfo}. + */ + public void updateModel(TargetInfo targetInfo) { + mResolverListController.updateModel(targetInfo); } - public void updateChooserCounts(String packageName, String action) { + /** + * Updates the model about Chooser Activity selection. + */ + public void updateChooserCounts(String packageName, String action, UserHandle userHandle) { mResolverListController.updateChooserCounts( - packageName, getUserHandle().getIdentifier(), action); + packageName, userHandle, action); } List getUnfilteredResolveList() { @@ -468,6 +478,7 @@ public class ResolverListAdapter extends BaseAdapter { ri.icon = 0; } + ri.userHandle = mInitialIntentsUserSpace; addResolveInfo(DisplayResolveInfo.newDisplayResolveInfo( ii, ri, @@ -761,8 +772,10 @@ public class ResolverListAdapter extends BaseAdapter { } Drawable loadIconForResolveInfo(ResolveInfo ri) { - // Load icons based on the current process. If in work profile icons should be badged. - return mPresentationFactory.makePresentationGetter(ri).getIcon(getUserHandle()); + // Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons + // should be badged. + return mPresentationFactory.makePresentationGetter(ri) + .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, getUserHandle())); } protected final Drawable loadIconPlaceholder() { diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java index b4544c43..d5a5fedf 100644 --- a/java/src/com/android/intentresolver/ResolverListController.java +++ b/java/src/com/android/intentresolver/ResolverListController.java @@ -32,8 +32,8 @@ import android.os.UserHandle; import android.util.Log; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.model.AbstractResolverComparator; -import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; @@ -58,34 +58,26 @@ public class ResolverListController { private static final String TAG = "ResolverListController"; private static final boolean DEBUG = false; + private final UserHandle mQueryIntentsAsUser; private AbstractResolverComparator mResolverComparator; private boolean isComputed = false; - public ResolverListController( - Context context, - PackageManager pm, - Intent targetIntent, - String referrerPackage, - int launchedFromUid) { - this(context, pm, targetIntent, referrerPackage, launchedFromUid, - new ResolverRankerServiceResolverComparator( - context, targetIntent, referrerPackage, null, null)); - } - public ResolverListController( Context context, PackageManager pm, Intent targetIntent, String referrerPackage, int launchedFromUid, - AbstractResolverComparator resolverComparator) { + AbstractResolverComparator resolverComparator, + UserHandle queryIntentsAsUser) { mContext = context; mpm = pm; mLaunchedFromUid = launchedFromUid; mTargetIntent = targetIntent; mReferrerPackage = referrerPackage; mResolverComparator = resolverComparator; + mQueryIntentsAsUser = queryIntentsAsUser; } @VisibleForTesting @@ -118,7 +110,8 @@ public class ResolverListController { | PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0) - | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0); + | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0) + | PackageManager.MATCH_CLONE_PROFILE; return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags); } @@ -154,6 +147,10 @@ public class ResolverListController { final int intoCount = into.size(); for (int i = 0; i < fromCount; i++) { final ResolveInfo newInfo = from.get(i); + if (newInfo.userHandle == null) { + Log.w(TAG, "Skipping ResolveInfo with no userHandle: " + newInfo); + continue; + } boolean found = false; // Only loop to the end of into as it was before we started; no dupes in from. for (int j = 0; j < intoCount; j++) { @@ -344,22 +341,28 @@ public class ResolverListController { @VisibleForTesting public float getScore(DisplayResolveInfo target) { - return mResolverComparator.getScore(target.getResolvedComponentName()); + return mResolverComparator.getScore(target); } /** * Returns the app share score of the given {@code componentName}. */ - public float getScore(ComponentName componentName) { - return mResolverComparator.getScore(componentName); + public float getScore(TargetInfo targetInfo) { + return mResolverComparator.getScore(targetInfo); } - public void updateModel(ComponentName componentName) { - mResolverComparator.updateModel(componentName); + /** + * Updates the model about the chosen {@code targetInfo}. + */ + public void updateModel(TargetInfo targetInfo) { + mResolverComparator.updateModel(targetInfo); } - public void updateChooserCounts(String packageName, int userId, String action) { - mResolverComparator.updateChooserCounts(packageName, userId, action); + /** + * Updates the model about Chooser Activity selection. + */ + public void updateChooserCounts(String packageName, UserHandle user, String action) { + mResolverComparator.updateChooserCounts(packageName, user, action); } public void destroy() { diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java index 48e3b62d..85d97ad5 100644 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java @@ -44,7 +44,8 @@ public class ResolverMultiProfilePagerAdapter extends ResolverListAdapter adapter, EmptyStateProvider emptyStateProvider, Supplier workProfileQuietModeChecker, - UserHandle workProfileUserHandle) { + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle) { this( context, ImmutableList.of(adapter), @@ -52,6 +53,7 @@ public class ResolverMultiProfilePagerAdapter extends workProfileQuietModeChecker, /* defaultProfile= */ 0, workProfileUserHandle, + cloneProfileUserHandle, new BottomPaddingOverrideSupplier()); } @@ -61,7 +63,8 @@ public class ResolverMultiProfilePagerAdapter extends EmptyStateProvider emptyStateProvider, Supplier workProfileQuietModeChecker, @Profile int defaultProfile, - UserHandle workProfileUserHandle) { + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle) { this( context, ImmutableList.of(personalAdapter, workAdapter), @@ -69,6 +72,7 @@ public class ResolverMultiProfilePagerAdapter extends workProfileQuietModeChecker, defaultProfile, workProfileUserHandle, + cloneProfileUserHandle, new BottomPaddingOverrideSupplier()); } @@ -79,6 +83,7 @@ public class ResolverMultiProfilePagerAdapter extends Supplier workProfileQuietModeChecker, @Profile int defaultProfile, UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { super( context, @@ -89,6 +94,7 @@ public class ResolverMultiProfilePagerAdapter extends workProfileQuietModeChecker, defaultProfile, workProfileUserHandle, + cloneProfileUserHandle, () -> (ViewGroup) LayoutInflater.from(context).inflate( R.layout.resolver_list_per_profile, null, false), bottomPaddingOverrideSupplier); diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java index ea767568..7357fde9 100644 --- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java @@ -32,11 +32,16 @@ import android.util.Log; import com.android.intentresolver.ChooserActivityLogger; import com.android.intentresolver.ResolvedComponentInfo; import com.android.intentresolver.ResolverActivity; +import com.android.intentresolver.chooser.TargetInfo; + +import com.google.android.collect.Lists; import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * Used to sort resolved activities in {@link ResolverListController}. @@ -50,8 +55,8 @@ public abstract class AbstractResolverComparator implements Comparator mPmMap = new HashMap<>(); + protected final Map mUsmMap = new HashMap<>(); protected String[] mAnnotations; protected String mContentType; @@ -100,14 +105,48 @@ public abstract class AbstractResolverComparator implements Comparator resolvedActivityUserSpaceList) { String scheme = intent.getScheme(); mHttp = "http".equals(scheme) || "https".equals(scheme); mContentType = intent.getType(); getContentAnnotations(intent); - mPm = context.getPackageManager(); - mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE); - mAzComparator = new AzInfoComparator(context); + for (UserHandle user : resolvedActivityUserSpaceList) { + Context userContext = launchedFromContext.createContextAsUser(user, 0); + mPmMap.put(user, userContext.getPackageManager()); + mUsmMap.put( + user, + (UsageStatsManager) userContext.getSystemService(Context.USAGE_STATS_SERVICE)); + } + mAzComparator = new AzInfoComparator(launchedFromContext); } // get annotations of content from intent. @@ -197,8 +236,8 @@ public abstract class AbstractResolverComparator implements ComparatorDefault implementation does nothing, as we could have simple model that does not train * online. * - * @param componentName the component that the user clicked + * * @param targetInfo the target that the user clicked. */ - public void updateModel(ComponentName componentName) { + public void updateModel(TargetInfo targetInfo) { } /** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */ diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java index c986ef15..84dca3ff 100644 --- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java @@ -33,6 +33,9 @@ import android.util.Log; import com.android.intentresolver.ChooserActivityLogger; import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.chooser.TargetInfo; + +import com.google.android.collect.Lists; import java.util.ArrayList; import java.util.Comparator; @@ -70,7 +73,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp AppPredictor appPredictor, UserHandle user, ChooserActivityLogger chooserActivityLogger) { - super(context, intent); + super(context, intent, Lists.newArrayList(user)); mContext = context; mIntent = intent; mAppPredictor = appPredictor; @@ -108,9 +111,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp // APS for chooser is disabled. Fallback to resolver. mResolverRankerService = new ResolverRankerServiceResolverComparator( - mContext, mIntent, mReferrerPackage, + mContext, + mIntent, + mReferrerPackage, () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), - getChooserActivityLogger()); + getChooserActivityLogger(), + mUser); mComparatorModel = buildUpdatedModel(); mResolverRankerService.compute(targets); } else { @@ -167,13 +173,13 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp } @Override - public float getScore(ComponentName name) { - return mComparatorModel.getScore(name); + public float getScore(TargetInfo targetInfo) { + return mComparatorModel.getScore(targetInfo); } @Override - public void updateModel(ComponentName componentName) { - mComparatorModel.notifyOnTargetSelected(componentName); + public void updateModel(TargetInfo targetInfo) { + mComparatorModel.notifyOnTargetSelected(targetInfo); } @Override @@ -246,11 +252,11 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp } @Override - public float getScore(ComponentName name) { + public float getScore(TargetInfo targetInfo) { if (mResolverRankerService != null) { - return mResolverRankerService.getScore(name); + return mResolverRankerService.getScore(targetInfo); } - Integer rank = mTargetRanks.get(name); + Integer rank = mTargetRanks.get(targetInfo.getResolvedComponentName()); if (rank == null) { Log.w(TAG, "Score requested for unknown component. Did you call compute yet?"); return 0f; @@ -260,18 +266,19 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp } @Override - public void notifyOnTargetSelected(ComponentName componentName) { + public void notifyOnTargetSelected(TargetInfo targetInfo) { if (mResolverRankerService != null) { - mResolverRankerService.updateModel(componentName); + mResolverRankerService.updateModel(targetInfo); return; } + ComponentName targetComponent = targetInfo.getResolvedComponentName(); + AppTargetId targetId = new AppTargetId(targetComponent.toString()); + AppTarget appTarget = + new AppTarget.Builder(targetId, targetComponent.getPackageName(), mUser) + .setClassName(targetComponent.getClassName()) + .build(); mAppPredictor.notifyAppTargetEvent( - new AppTargetEvent.Builder( - new AppTarget.Builder( - new AppTargetId(componentName.toString()), - componentName.getPackageName(), mUser) - .setClassName(componentName.getClassName()).build(), - ACTION_LAUNCH).build()); + new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build()); } } } diff --git a/java/src/com/android/intentresolver/model/ResolverComparatorModel.java b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java index 3616a853..4835ea17 100644 --- a/java/src/com/android/intentresolver/model/ResolverComparatorModel.java +++ b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java @@ -16,9 +16,10 @@ package com.android.intentresolver.model; -import android.content.ComponentName; import android.content.pm.ResolveInfo; +import com.android.intentresolver.chooser.TargetInfo; + import java.util.Comparator; /** @@ -44,7 +45,7 @@ interface ResolverComparatorModel { * likelihood that the user will select that component as the target. Implementations that don't * assign numerical scores are recommended to return a value of 0 for all components. */ - float getScore(ComponentName name); + float getScore(TargetInfo targetInfo); /** * Notify the model that the user selected a target. (Models may log this information, use it as @@ -52,5 +53,5 @@ interface ResolverComparatorModel { * {@code ResolverComparatorModel} instance is immutable, clients will need to get an up-to-date * instance in order to see any changes in the ranking that might result from this feedback. */ - void notifyOnTargetSelected(ComponentName componentName); + void notifyOnTargetSelected(TargetInfo targetInfo); } diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java index 0431078c..725212e4 100644 --- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java @@ -17,11 +17,13 @@ package com.android.intentresolver.model; +import android.annotation.Nullable; import android.app.usage.UsageStats; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -39,12 +41,16 @@ import android.util.Log; import com.android.intentresolver.ChooserActivityLogger; import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.chooser.TargetInfo; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.google.android.collect.Lists; + import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -70,10 +76,10 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200; private final Collator mCollator; - private final Map mStats; + private final Map> mStatsPerUser; private final long mCurrentTime; private final long mSinceTime; - private final LinkedHashMap mTargetsDict = new LinkedHashMap<>(); + private final Map> mTargetsDictPerUser; private final String mReferrerPackage; private final Object mLock = new Object(); private ArrayList mTargets; @@ -86,17 +92,48 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom private CountDownLatch mConnectSignal; private ResolverRankerServiceComparatorModel mComparatorModel; - public ResolverRankerServiceResolverComparator(Context context, Intent intent, - String referrerPackage, Runnable afterCompute, - ChooserActivityLogger chooserActivityLogger) { - super(context, intent); - mCollator = Collator.getInstance(context.getResources().getConfiguration().locale); + /** + * Constructor to initialize the comparator. + * @param launchedFromContext the activity calling this comparator + * @param intent original intent + * @param targetUserSpace the userSpace(s) used by the comparator for fetching activity stats + * and recording activity selection. The latter could be different from + * the userSpace provided by context. + */ + public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, + String referrerPackage, Runnable afterCompute, + ChooserActivityLogger chooserActivityLogger, UserHandle targetUserSpace) { + this(launchedFromContext, intent, referrerPackage, afterCompute, chooserActivityLogger, + Lists.newArrayList(targetUserSpace)); + } + + /** + * Constructor to initialize the comparator. + * @param launchedFromContext the activity calling this comparator + * @param intent original intent + * @param targetUserSpaceList the userSpace(s) used by the comparator for fetching activity + * stats and recording activity selection. The latter could be + * different from the userSpace provided by context. + */ + public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, + String referrerPackage, Runnable afterCompute, + ChooserActivityLogger chooserActivityLogger, List targetUserSpaceList) { + super(launchedFromContext, intent, targetUserSpaceList); + mCollator = Collator.getInstance( + launchedFromContext.getResources().getConfiguration().locale); mReferrerPackage = referrerPackage; - mContext = context; + mContext = launchedFromContext; mCurrentTime = System.currentTimeMillis(); mSinceTime = mCurrentTime - USAGE_STATS_PERIOD; - mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime); + mStatsPerUser = new HashMap<>(); + mTargetsDictPerUser = new HashMap<>(); + for (UserHandle user : targetUserSpaceList) { + mStatsPerUser.put( + user, + mUsmMap.get(user).queryAndAggregateUsageStats(mSinceTime, mCurrentTime)); + mTargetsDictPerUser.put(user, new LinkedHashMap<>()); + } mAction = intent.getAction(); mRankerServiceName = new ComponentName(mContext, this.getClass()); setCallBack(afterCompute); @@ -147,58 +184,68 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom float mostChooserScore = 1.0f; for (ResolvedComponentInfo target : targets) { + if (target.getResolveInfoAt(0) == null) { + continue; + } final ResolverTarget resolverTarget = new ResolverTarget(); - mTargetsDict.put(target.name, resolverTarget); - final UsageStats pkStats = mStats.get(target.name.getPackageName()); - if (pkStats != null) { - // Only count recency for apps that weren't the caller - // since the caller is always the most recent. - // Persistent processes muck this up, so omit them too. - if (!target.name.getPackageName().equals(mReferrerPackage) - && !isPersistentProcess(target)) { - final float recencyScore = - (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0); - resolverTarget.setRecencyScore(recencyScore); - if (recencyScore > mostRecencyScore) { - mostRecencyScore = recencyScore; + final UserHandle resolvedComponentUserSpace = + target.getResolveInfoAt(0).userHandle; + final Map targetsDict = + mTargetsDictPerUser.get(resolvedComponentUserSpace); + final Map stats = mStatsPerUser.get(resolvedComponentUserSpace); + if (targetsDict != null && stats != null) { + targetsDict.put(target.name, resolverTarget); + final UsageStats pkStats = stats.get(target.name.getPackageName()); + if (pkStats != null) { + // Only count recency for apps that weren't the caller + // since the caller is always the most recent. + // Persistent processes muck this up, so omit them too. + if (!target.name.getPackageName().equals(mReferrerPackage) + && !isPersistentProcess(target)) { + final float recencyScore = + (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0); + resolverTarget.setRecencyScore(recencyScore); + if (recencyScore > mostRecencyScore) { + mostRecencyScore = recencyScore; + } + } + final float timeSpentScore = (float) pkStats.getTotalTimeInForeground(); + resolverTarget.setTimeSpentScore(timeSpentScore); + if (timeSpentScore > mostTimeSpentScore) { + mostTimeSpentScore = timeSpentScore; + } + final float launchScore = (float) pkStats.mLaunchCount; + resolverTarget.setLaunchScore(launchScore); + if (launchScore > mostLaunchScore) { + mostLaunchScore = launchScore; } - } - final float timeSpentScore = (float) pkStats.getTotalTimeInForeground(); - resolverTarget.setTimeSpentScore(timeSpentScore); - if (timeSpentScore > mostTimeSpentScore) { - mostTimeSpentScore = timeSpentScore; - } - final float launchScore = (float) pkStats.mLaunchCount; - resolverTarget.setLaunchScore(launchScore); - if (launchScore > mostLaunchScore) { - mostLaunchScore = launchScore; - } - float chooserScore = 0.0f; - if (pkStats.mChooserCounts != null && mAction != null - && pkStats.mChooserCounts.get(mAction) != null) { - chooserScore = (float) pkStats.mChooserCounts.get(mAction) - .getOrDefault(mContentType, 0); - if (mAnnotations != null) { - final int size = mAnnotations.length; - for (int i = 0; i < size; i++) { - chooserScore += (float) pkStats.mChooserCounts.get(mAction) - .getOrDefault(mAnnotations[i], 0); + float chooserScore = 0.0f; + if (pkStats.mChooserCounts != null && mAction != null + && pkStats.mChooserCounts.get(mAction) != null) { + chooserScore = (float) pkStats.mChooserCounts.get(mAction) + .getOrDefault(mContentType, 0); + if (mAnnotations != null) { + final int size = mAnnotations.length; + for (int i = 0; i < size; i++) { + chooserScore += (float) pkStats.mChooserCounts.get(mAction) + .getOrDefault(mAnnotations[i], 0); + } } } - } - if (DEBUG) { - if (mAction == null) { - Log.d(TAG, "Action type is null"); - } else { - Log.d(TAG, "Chooser Count of " + mAction + ":" - + target.name.getPackageName() + " is " - + Float.toString(chooserScore)); + if (DEBUG) { + if (mAction == null) { + Log.d(TAG, "Action type is null"); + } else { + Log.d(TAG, "Chooser Count of " + mAction + ":" + + target.name.getPackageName() + " is " + + Float.toString(chooserScore)); + } + } + resolverTarget.setChooserScore(chooserScore); + if (chooserScore > mostChooserScore) { + mostChooserScore = chooserScore; } - } - resolverTarget.setChooserScore(chooserScore); - if (chooserScore > mostChooserScore) { - mostChooserScore = chooserScore; } } } @@ -210,7 +257,10 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom + " mostChooserScore: " + mostChooserScore); } - mTargets = new ArrayList<>(mTargetsDict.values()); + mTargets = new ArrayList<>(); + for (UserHandle u : mTargetsDictPerUser.keySet()) { + mTargets.addAll(mTargetsDictPerUser.get(u).values()); + } for (ResolverTarget target : mTargets) { final float recency = target.getRecencyScore() / mostRecencyScore; setFeatures(target, recency * recency * RECENCY_MULTIPLIER, @@ -233,15 +283,15 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom } @Override - public float getScore(ComponentName name) { - return mComparatorModel.getScore(name); + public float getScore(TargetInfo targetInfo) { + return mComparatorModel.getScore(targetInfo); } // update ranking model when the connection to it is valid. @Override - public void updateModel(ComponentName componentName) { + public void updateModel(TargetInfo targetInfo) { synchronized (mLock) { - mComparatorModel.notifyOnTargetSelected(componentName); + mComparatorModel.notifyOnTargetSelected(targetInfo); } } @@ -282,7 +332,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom // resolve the service for ranking. private Intent resolveRankerService() { Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE); - final List resolveInfos = mPm.queryIntentServices(intent, 0); + final List resolveInfos = mContext.getPackageManager() + .queryIntentServices(intent, 0); for (ResolveInfo resolveInfo : resolveInfos) { if (resolveInfo == null || resolveInfo.serviceInfo == null || resolveInfo.serviceInfo.applicationInfo == null) { @@ -295,7 +346,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom resolveInfo.serviceInfo.applicationInfo.packageName, resolveInfo.serviceInfo.name); try { - final String perm = mPm.getServiceInfo(componentName, 0).permission; + final String perm = + mContext.getPackageManager().getServiceInfo(componentName, 0).permission; if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) { Log.w(TAG, "ResolverRankerService " + componentName + " does not require" + " permission " + ResolverRankerService.BIND_PERMISSION @@ -306,9 +358,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom + " in the manifest."); continue; } - if (PackageManager.PERMISSION_GRANTED != mPm.checkPermission( - ResolverRankerService.HOLD_PERMISSION, - resolveInfo.serviceInfo.packageName)) { + if (PackageManager.PERMISSION_GRANTED != mContext.getPackageManager() + .checkPermission(ResolverRankerService.HOLD_PERMISSION, + resolveInfo.serviceInfo.packageName)) { Log.w(TAG, "ResolverRankerService " + componentName + " does not hold" + " permission " + ResolverRankerService.HOLD_PERMISSION + " - this service will not be queried for " @@ -386,7 +438,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom @Override void beforeCompute() { super.beforeCompute(); - mTargetsDict.clear(); + for (UserHandle userHandle : mTargetsDictPerUser.keySet()) { + mTargetsDictPerUser.get(userHandle).clear(); + } mTargets = null; mRankerServiceName = new ComponentName(mContext, this.getClass()); mComparatorModel = buildUpdatedModel(); @@ -468,14 +522,14 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom // so the ResolverComparatorModel may provide inconsistent results. We should make immutable // copies of the data (waiting for any necessary remaining data before creating the model). return new ResolverRankerServiceComparatorModel( - mStats, - mTargetsDict, + mStatsPerUser, + mTargetsDictPerUser, mTargets, mCollator, mRanker, mRankerServiceName, (mAnnotations != null), - mPm); + mPmMap); } /** @@ -484,35 +538,36 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom * removing the complex legacy API. */ static class ResolverRankerServiceComparatorModel implements ResolverComparatorModel { - private final Map mStats; // Treat as immutable. - private final Map mTargetsDict; // Treat as immutable. + private final Map> mStatsPerUser; // Treat as immutable. + // Treat as immutable. + private final Map> mTargetsDictPerUser; private final List mTargets; // Treat as immutable. private final Collator mCollator; private final IResolverRankerService mRanker; private final ComponentName mRankerServiceName; private final boolean mAnnotationsUsed; - private final PackageManager mPm; + private final Map mPmMap; // TODO: it doesn't look like we should have to pass both targets and targetsDict, but it's // not written in a way that makes it clear whether we can derive one from the other (at // least in this constructor). ResolverRankerServiceComparatorModel( - Map stats, - Map targetsDict, + Map> statsPerUser, + Map> targetsDictPerUser, List targets, Collator collator, IResolverRankerService ranker, ComponentName rankerServiceName, boolean annotationsUsed, - PackageManager pm) { - mStats = stats; - mTargetsDict = targetsDict; + Map pmMap) { + mStatsPerUser = statsPerUser; + mTargetsDictPerUser = targetsDictPerUser; mTargets = targets; mCollator = collator; mRanker = ranker; mRankerServiceName = rankerServiceName; mAnnotationsUsed = annotationsUsed; - mPm = pm; + mPmMap = pmMap; } @Override @@ -521,25 +576,29 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom // a bug there, or do we have a way of knowing it will be non-null under certain // conditions? return (lhs, rhs) -> { - if (mStats != null) { - final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName( - lhs.activityInfo.packageName, lhs.activityInfo.name)); - final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName( - rhs.activityInfo.packageName, rhs.activityInfo.name)); - - if (lhsTarget != null && rhsTarget != null) { - final int selectProbabilityDiff = Float.compare( - rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability()); - - if (selectProbabilityDiff != 0) { - return selectProbabilityDiff > 0 ? 1 : -1; - } + final ResolverTarget lhsTarget = + getActivityResolverTargetForUser(lhs.activityInfo, lhs.userHandle); + final ResolverTarget rhsTarget = + getActivityResolverTargetForUser(rhs.activityInfo, rhs.userHandle); + + if (lhsTarget != null && rhsTarget != null) { + final int selectProbabilityDiff = Float.compare( + rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability()); + + if (selectProbabilityDiff != 0) { + return selectProbabilityDiff > 0 ? 1 : -1; } } - CharSequence sa = lhs.loadLabel(mPm); + CharSequence sa = null; + if (mPmMap.containsKey(lhs.userHandle)) { + sa = lhs.loadLabel(mPmMap.get(lhs.userHandle)); + } if (sa == null) sa = lhs.activityInfo.name; - CharSequence sb = rhs.loadLabel(mPm); + CharSequence sb = null; + if (mPmMap.containsKey(rhs.userHandle)) { + sb = rhs.loadLabel(mPmMap.get(rhs.userHandle)); + } if (sb == null) sb = rhs.activityInfo.name; return mCollator.compare(sa.toString().trim(), sb.toString().trim()); @@ -547,8 +606,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom } @Override - public float getScore(ComponentName name) { - final ResolverTarget target = mTargetsDict.get(name); + public float getScore(TargetInfo targetInfo) { + ResolverTarget target = getResolverTargetForUserAndComponent( + targetInfo.getResolvedComponentName(), targetInfo.getResolveInfo().userHandle); if (target != null) { return target.getSelectProbability(); } @@ -556,13 +616,17 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom } @Override - public void notifyOnTargetSelected(ComponentName componentName) { + public void notifyOnTargetSelected(TargetInfo targetInfo) { if (mRanker != null) { try { - int selectedPos = new ArrayList(mTargetsDict.keySet()) - .indexOf(componentName); + int selectedPos = -1; + if (mTargetsDictPerUser.containsKey(targetInfo.getResolveInfo().userHandle)) { + selectedPos = new ArrayList<>(mTargetsDictPerUser + .get(targetInfo.getResolveInfo().userHandle).keySet()) + .indexOf(targetInfo.getResolvedComponentName()); + } if (selectedPos >= 0 && mTargets != null) { - final float selectedProbability = getScore(componentName); + final float selectedProbability = getScore(targetInfo); int order = 0; for (ResolverTarget target : mTargets) { if (target.getSelectProbability() > selectedProbability) { @@ -573,7 +637,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom mRanker.train(mTargets, selectedPos); } else { if (DEBUG) { - Log.d(TAG, "Selected a unknown component: " + componentName); + Log.d(TAG, "Selected a unknown component: " + targetInfo + .getResolvedComponentName()); } } } catch (RemoteException e) { @@ -597,5 +662,21 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom metricsLogger.write(log); } } + + @Nullable + private ResolverTarget getActivityResolverTargetForUser( + ActivityInfo activity, UserHandle user) { + return getResolverTargetForUserAndComponent( + new ComponentName(activity.packageName, activity.name), user); + } + + @Nullable + private ResolverTarget getResolverTargetForUserAndComponent( + ComponentName targetComponentName, UserHandle user) { + if ((mStatsPerUser == null) || !mTargetsDictPerUser.containsKey(user)) { + return null; + } + return mTargetsDictPerUser.get(user).get(targetComponentName); + } } } diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index f0c459e5..f9093b8f 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -28,7 +28,6 @@ import android.graphics.Bitmap; import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.shortcuts.ShortcutLoader; @@ -55,6 +54,7 @@ public class ChooserActivityOverrideData { @SuppressWarnings("Since15") public Function createPackageManager; + public Function onSafelyStartInternalCallback; public Function onSafelyStartCallback; public Function2, ShortcutLoader> shortcutLoaderFactory = (userHandle, callback) -> null; @@ -69,17 +69,18 @@ public class ChooserActivityOverrideData { public int alternateProfileSetting; public Resources resources; public UserHandle workProfileUserHandle; + public UserHandle cloneProfileUserHandle; + public UserHandle tabOwnerUserHandleForLaunch; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public Integer myUserId; public WorkProfileAvailabilityManager mWorkProfileAvailability; - public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public PackageManager packageManager; public FeatureFlagRepository featureFlagRepository; public void reset() { - onSafelyStartCallback = null; + onSafelyStartInternalCallback = null; isVoiceInteraction = null; createPackageManager = null; previewThumbnail = null; @@ -92,6 +93,8 @@ public class ChooserActivityOverrideData { alternateProfileSetting = 0; resources = null; workProfileUserHandle = null; + cloneProfileUserHandle = null; + tabOwnerUserHandleForLaunch = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; myUserId = null; @@ -122,13 +125,6 @@ public class ChooserActivityOverrideData { }; shortcutLoaderFactory = ((userHandle, resultConsumer) -> null); - mMyUserIdProvider = new MyUserIdProvider() { - @Override - public int getMyUserId() { - return myUserId != null ? myUserId : UserHandle.myUserId(); - } - }; - mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) .thenAnswer(invocation -> hasCrossProfileIntents); diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt index 58f6b733..9504f377 100644 --- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -20,6 +20,7 @@ import android.content.ComponentName import android.content.Intent import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags +import android.os.UserHandle import android.view.View import android.widget.FrameLayout import android.widget.ImageView @@ -39,6 +40,9 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class ChooserListAdapterTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val packageManager = mock { whenever( resolveActivity(any(), any()) @@ -63,7 +67,8 @@ class ChooserListAdapterTest { packageManager, chooserActivityLogger, mock(), - 0 + 0, + null ) { override fun createLoadDirectShareIconTask( info: SelectableTargetInfo @@ -119,7 +124,7 @@ class ChooserListAdapterTest { SelectableTargetInfo.newSelectableTargetInfo( /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(2, 0), + ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE), "label", "extended info", Intent(), diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index d4ae666b..8886892f 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -29,10 +29,10 @@ import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.database.Cursor; import android.net.Uri; +import android.os.Bundle; import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.flags.FeatureFlagRepository; @@ -80,14 +80,15 @@ public class ChooserWrapperActivity initialIntents, rList, filterLastUsed, - resolverListController, + createListController(userHandle), userHandle, targetIntent, this, packageManager, getChooserActivityLogger(), chooserRequest, - maxTargetsPerRow); + maxTargetsPerRow, + userHandle); } @Override @@ -141,14 +142,6 @@ public class ChooserWrapperActivity return super.isVoiceInteraction(); } - @Override - protected MyUserIdProvider createMyUserIdProvider() { - if (sOverrides.mMyUserIdProvider != null) { - return sOverrides.mMyUserIdProvider; - } - return super.createMyUserIdProvider(); - } - @Override protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { if (sOverrides.mCrossProfileIntentsChecker != null) { @@ -166,12 +159,13 @@ public class ChooserWrapperActivity } @Override - public void safelyStartActivity(TargetInfo cti) { - if (sOverrides.onSafelyStartCallback != null - && sOverrides.onSafelyStartCallback.apply(cti)) { + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(cti)) { return; } - super.safelyStartActivity(cti); + super.safelyStartActivityInternal(cti, user, options); } @Override @@ -259,6 +253,14 @@ public class ChooserWrapperActivity return mMultiProfilePagerAdapter.getCurrentUserHandle(); } + @Override + protected UserHandle getTabOwnerUserHandleForLaunch() { + if (sOverrides.tabOwnerUserHandleForLaunch == null) { + return super.getTabOwnerUserHandleForLaunch(); + } + return sOverrides.tabOwnerUserHandleForLaunch; + } + @Override public Context createContextAsUser(UserHandle user, int flags) { // return the current context as a work profile doesn't really exist in these tests diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java index e2772423..12b71970 100644 --- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java +++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java @@ -56,6 +56,8 @@ import androidx.test.runner.AndroidJUnit4; import com.android.intentresolver.widget.ResolverDrawerLayout; +import com.google.android.collect.Lists; + import org.junit.Before; import org.junit.Ignore; import org.junit.Rule; @@ -71,6 +73,9 @@ import java.util.List; */ @RunWith(AndroidJUnit4.class) public class ResolverActivityTest { + + private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app + .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser(); protected Intent getConcreteIntentForLaunch(Intent clientIntent) { clientIntent.setClass( androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(), @@ -97,7 +102,8 @@ public class ResolverActivityTest { @Test public void twoOptionsAndUserSelectsOne() throws InterruptedException { Intent sendIntent = createSendImageIntent(); - List resolvedComponentInfos = createResolvedComponentsForTest(2); + List resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); @@ -108,8 +114,8 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -126,7 +132,8 @@ public class ResolverActivityTest { @Test public void setMaxHeight() throws Exception { Intent sendIntent = createSendImageIntent(); - List resolvedComponentInfos = createResolvedComponentsForTest(2); + List resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); waitForIdle(); @@ -168,7 +175,8 @@ public class ResolverActivityTest { @Test public void setShowAtTopToTrue() throws Exception { Intent sendIntent = createSendImageIntent(); - List resolvedComponentInfos = createResolvedComponentsForTest(2); + List resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); waitForIdle(); @@ -198,7 +206,8 @@ public class ResolverActivityTest { @Test public void hasLastChosenActivity() throws Exception { Intent sendIntent = createSendImageIntent(); - List resolvedComponentInfos = createResolvedComponentsForTest(2); + List resolvedComponentInfos = createResolvedComponentsForTest(2, + PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); @@ -213,8 +222,8 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getPlaceholderCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -226,10 +235,12 @@ public class ResolverActivityTest { @Test public void hasOtherProfileOneOption() throws Exception { List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); - setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); markWorkProfileUserAvailable(); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0); Intent sendIntent = createSendImageIntent(); @@ -241,13 +252,14 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; // Make a stable copy of the components as the original list may be modified List stableCopy = - createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10); + createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10, + PERSONAL_USER_HANDLE); // We pick the first one as there is another one in the work profile side onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))) .perform(click()); @@ -261,7 +273,7 @@ public class ResolverActivityTest { public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception { Intent sendIntent = createSendImageIntent(); List resolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); @@ -274,8 +286,8 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -284,7 +296,7 @@ public class ResolverActivityTest { // Make a stable copy of the components as the original list may be modified List stableCopy = - createResolvedComponentsForTestWithOtherProfile(2); + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); @@ -300,7 +312,7 @@ public class ResolverActivityTest { // chosen activity. Intent sendIntent = createSendImageIntent(); List resolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0); setupResolverControllers(resolvedComponentInfos); @@ -315,8 +327,8 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -325,7 +337,7 @@ public class ResolverActivityTest { // Make a stable copy of the components as the original list may be modified List stableCopy = - createResolvedComponentsForTestWithOtherProfile(2); + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) .perform(click()); @@ -358,12 +370,14 @@ public class ResolverActivityTest { @Test public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException { List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10, + PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>(workResolvedComponentInfos)); Intent sendIntent = createSendImageIntent(); - markWorkProfileUserAvailable(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); @@ -376,8 +390,11 @@ public class ResolverActivityTest { @Test public void testWorkTab_workTabUsesExpectedAdapter() { List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); markWorkProfileUserAvailable(); @@ -393,11 +410,12 @@ public class ResolverActivityTest { @Test public void testWorkTab_personalTabUsesExpectedAdapter() { List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE); + markWorkProfileUserAvailable(); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); - markWorkProfileUserAvailable(); final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); waitForIdle(); @@ -411,8 +429,10 @@ public class ResolverActivityTest { public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -429,13 +449,15 @@ public class ResolverActivityTest { public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -459,8 +481,9 @@ public class ResolverActivityTest { throws InterruptedException { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -477,8 +500,9 @@ public class ResolverActivityTest { public void testWorkTab_headerIsVisibleInPersonalTab() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createOpenWebsiteIntent(); @@ -494,8 +518,9 @@ public class ResolverActivityTest { public void testWorkTab_switchTabs_headerStaysSame() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(1); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createOpenWebsiteIntent(); @@ -519,13 +544,15 @@ public class ResolverActivityTest { throws InterruptedException { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10); - List workResolvedComponentInfos = createResolvedComponentsForTest(4); + createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10, + PERSONAL_USER_HANDLE); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -551,9 +578,11 @@ public class ResolverActivityTest { markWorkProfileUserAvailable(); int workProfileTargets = 4; List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.hasCrossProfileIntents = false; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -575,9 +604,11 @@ public class ResolverActivityTest { markWorkProfileUserAvailable(); int workProfileTargets = 4; List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10, + PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.isQuietModeEnabled = true; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); @@ -598,9 +629,9 @@ public class ResolverActivityTest { public void testWorkTab_noWorkAppsAvailable_emptyStateShown() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -620,9 +651,9 @@ public class ResolverActivityTest { public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -644,9 +675,9 @@ public class ResolverActivityTest { public void testMiniResolver() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); // Personal profile only has a browser personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); @@ -662,9 +693,9 @@ public class ResolverActivityTest { public void testMiniResolver_noCurrentProfileTarget() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(1); + createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -690,9 +721,9 @@ public class ResolverActivityTest { public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() { markWorkProfileUserAvailable(); List personalResolvedComponentInfos = - createResolvedComponentsForTest(3); + createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(0); + createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); @@ -714,16 +745,18 @@ public class ResolverActivityTest { markWorkProfileUserAvailable(); int workProfileTargets = 4; List personalResolvedComponentInfos = - createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10); + createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10, + PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, + sOverrides.workProfileUserHandle); sOverrides.hasCrossProfileIntents = false; setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendImageIntent(); sendIntent.setType("TestType"); ResolveInfo[] chosen = new ResolveInfo[1]; - sOverrides.onSafelyStartCallback = targetInfo -> { - chosen[0] = targetInfo.getResolveInfo(); + sOverrides.onSafelyStartInternalCallback = result -> { + chosen[0] = result.first.getResolveInfo(); return true; }; @@ -741,7 +774,7 @@ public class ResolverActivityTest { // chosen activity. Intent sendIntent = createSendImageIntent(); List resolvedComponentInfos = - createResolvedComponentsForTest(2); + createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE); setupResolverControllers(resolvedComponentInfos); when(sOverrides.resolverListController.getLastChosen()) @@ -757,6 +790,200 @@ public class ResolverActivityTest { assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); } + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markCloneProfileUserAvailable(); + List resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List workResolvedComponentInfos = createResolvedComponentsForTest(4, + sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle())); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 2, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource()); + waitForIdle(); + + assertThat(activity.getAdapter().hasFilteredItem(), is(false)); + assertThat(activity.getAdapter().getCount(), is(2)); + assertThat(activity.getAdapter().getPlaceholderCount(), is(2)); + } + + @Test + public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + Intent sendIntent = createSendImageIntent(); + List resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + + setupResolverControllers(resolvedComponentInfos); + when(sOverrides.resolverListController.getLastChosen()) + .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0)); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + + // Confirm that the button bar is disabled by default + onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled()))); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + + // Make a stable copy of the components as the original list may be modified + List stableCopy = + createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE); + + onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)) + .perform(click()); + + onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled())); + onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled()))); + } + + @Test + public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser() + throws Exception { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + + List personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + sOverrides.hasCrossProfileIntents = false; + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(first(allOf(withText(personalResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser() + throws Exception { + markWorkProfileUserAvailable(); + // enable cloneProfile + markCloneProfileUserAvailable(); + + List personalResolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + List workResolvedComponentInfos = + createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + sendIntent.setType("TestType"); + final UserHandle[] selectedActivityUserHandle = new UserHandle[1]; + sOverrides.onSafelyStartInternalCallback = result -> { + selectedActivityUserHandle[0] = result.second; + return true; + }; + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + onView(withText(R.string.resolver_work_tab)) + .perform(click()); + waitForIdle(); + onView(first(allOf(withText(workResolvedComponentInfos.get(0) + .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed()))) + .perform(click()); + onView(withId(com.android.internal.R.id.button_once)) + .perform(click()); + waitForIdle(); + + assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle())); + } + + @Test + public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers() + throws Exception { + // enable cloneProfile + markCloneProfileUserAvailable(); + List resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendImageIntent(); + + final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent); + waitForIdle(); + List result = activity + .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE); + + assertThat(result.containsAll(Lists.newArrayList(PERSONAL_USER_HANDLE, + sOverrides.cloneProfileUserHandle)), is(true)); + } + private Intent createSendImageIntent() { Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); @@ -772,36 +999,56 @@ public class ResolverActivityTest { return sendIntent; } - private List createResolvedComponentsForTest(int numberOfResults) { + private List createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); + } + return infoList; + } + + private List createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); } return infoList; } private List createResolvedComponentsForTestWithOtherProfile( - int numberOfResults) { + int numberOfResults, + UserHandle resolvedForUser) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + resolvedForUser)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } } return infoList; } private List createResolvedComponentsForTestWithOtherProfile( - int numberOfResults, int userId) { + int numberOfResults, int userId, UserHandle resolvedForUser) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + resolvedForUser)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } } return infoList; @@ -820,6 +1067,10 @@ public class ResolverActivityTest { setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); } + private void markCloneProfileUserAvailable() { + ResolverWrapperActivity.sOverrides.cloneProfileUserHandle = UserHandle.of(11); + } + private void setupResolverControllers( List personalResolvedComponentInfos, List workResolvedComponentInfos) { diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java index b6b32b5a..688dd867 100644 --- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java +++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java @@ -43,6 +43,14 @@ public class ResolverDataProvider { createResolveInfo(i, UserHandle.USER_CURRENT)); } + static ResolvedComponentInfo createResolvedComponentInfo(int i, + UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, UserHandle.USER_CURRENT, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfo( ComponentName componentName, Intent intent) { return new ResolvedComponentInfo( @@ -51,6 +59,14 @@ public class ResolverDataProvider { createResolveInfo(componentName, UserHandle.USER_CURRENT)); } + static ResolvedComponentInfo createResolvedComponentInfo( + ComponentName componentName, Intent intent, UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + componentName, + intent, + createResolveInfo(componentName, UserHandle.USER_CURRENT, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) { return new ResolvedComponentInfo( createComponentName(i), @@ -58,6 +74,14 @@ public class ResolverDataProvider { createResolveInfo(i, USER_SOMEONE_ELSE)); } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, + UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, USER_SOMEONE_ELSE, resolvedForUser)); + } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) { return new ResolvedComponentInfo( createComponentName(i), @@ -65,6 +89,14 @@ public class ResolverDataProvider { createResolveInfo(i, userId)); } + static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, + int userId, UserHandle resolvedForUser) { + return new ResolvedComponentInfo( + createComponentName(i), + createResolverIntent(i), + createResolveInfo(i, userId, resolvedForUser)); + } + public static ComponentName createComponentName(int i) { final String name = "component" + i; return new ComponentName("foo.bar." + name, name); @@ -76,6 +108,13 @@ public class ResolverDataProvider { resolveInfo.targetUserId = userId; return resolveInfo; } + public static ResolveInfo createResolveInfo(int i, int userId, UserHandle resolvedForUser) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(i); + resolveInfo.targetUserId = userId; + resolveInfo.userHandle = resolvedForUser; + return resolveInfo; + } public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) { final ResolveInfo resolveInfo = new ResolveInfo(); @@ -84,6 +123,15 @@ public class ResolverDataProvider { return resolveInfo; } + public static ResolveInfo createResolveInfo(ComponentName componentName, int userId, + UserHandle resolvedForUser) { + final ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = createActivityInfo(componentName); + resolveInfo.targetUserId = userId; + resolveInfo.userHandle = resolvedForUser; + return resolveInfo; + } + static ActivityInfo createActivityInfo(int i) { ActivityInfo ai = new ActivityInfo(); ai.name = "activity_name" + i; diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java index d67b73af..645e8c72 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java @@ -21,6 +21,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.annotation.Nullable; import android.app.usage.UsageStatsManager; import android.content.Context; import android.content.Intent; @@ -28,9 +29,9 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.os.Bundle; import android.os.UserHandle; +import android.util.Pair; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; -import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider; import com.android.intentresolver.chooser.TargetInfo; import java.util.List; @@ -67,15 +68,8 @@ public class ResolverWrapperActivity extends ResolverActivity { createListController(userHandle), userHandle, payloadIntents.get(0), // TODO: extract upstream - this); - } - - @Override - protected MyUserIdProvider createMyUserIdProvider() { - if (sOverrides.mMyUserIdProvider != null) { - return sOverrides.mMyUserIdProvider; - } - return super.createMyUserIdProvider(); + this, + userHandle); } @Override @@ -118,12 +112,13 @@ public class ResolverWrapperActivity extends ResolverActivity { } @Override - public void safelyStartActivity(TargetInfo cti) { - if (sOverrides.onSafelyStartCallback != null && - sOverrides.onSafelyStartCallback.apply(cti)) { + public void safelyStartActivityInternal(TargetInfo cti, UserHandle user, + @Nullable Bundle options) { + if (sOverrides.onSafelyStartInternalCallback != null + && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) { return; } - super.safelyStartActivity(cti); + super.safelyStartActivityInternal(cti, user, options); } @Override @@ -151,11 +146,22 @@ public class ResolverWrapperActivity extends ResolverActivity { return sOverrides.workProfileUserHandle; } + @Override + protected UserHandle getCloneProfileUserHandle() { + return sOverrides.cloneProfileUserHandle; + } + @Override public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) { super.startActivityAsUser(intent, options, user); } + @Override + protected List getResolverRankerServiceUserHandleListInternal(UserHandle + userHandle) { + return super.getResolverRankerServiceUserHandleListInternal(userHandle); + } + /** * We cannot directly mock the activity created since instrumentation creates it. *

    @@ -164,25 +170,28 @@ public class ResolverWrapperActivity extends ResolverActivity { static class OverrideData { @SuppressWarnings("Since15") public Function createPackageManager; - public Function onSafelyStartCallback; + public Function, Boolean> onSafelyStartInternalCallback; public ResolverListController resolverListController; public ResolverListController workResolverListController; public Boolean isVoiceInteraction; public UserHandle workProfileUserHandle; + public UserHandle cloneProfileUserHandle; + public UserHandle tabOwnerUserHandleForLaunch; public Integer myUserId; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; public WorkProfileAvailabilityManager mWorkProfileAvailability; - public MyUserIdProvider mMyUserIdProvider; public CrossProfileIntentsChecker mCrossProfileIntentsChecker; public void reset() { - onSafelyStartCallback = null; + onSafelyStartInternalCallback = null; isVoiceInteraction = null; createPackageManager = null; resolverListController = mock(ResolverListController.class); workResolverListController = mock(ResolverListController.class); workProfileUserHandle = null; + cloneProfileUserHandle = null; + tabOwnerUserHandleForLaunch = null; myUserId = null; hasCrossProfileIntents = true; isQuietModeEnabled = false; @@ -212,13 +221,6 @@ public class ResolverWrapperActivity extends ResolverActivity { } }; - mMyUserIdProvider = new MyUserIdProvider() { - @Override - public int getMyUserId() { - return myUserId != null ? myUserId : UserHandle.myUserId(); - } - }; - mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class); when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt())) .thenAnswer(invocation -> hasCrossProfileIntents); diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java index a53b41d1..fd310fd8 100644 --- a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java +++ b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java @@ -41,7 +41,8 @@ public class ResolverWrapperAdapter extends ResolverListAdapter { ResolverListController resolverListController, UserHandle userHandle, Intent targetIntent, - ResolverListCommunicator resolverListCommunicator) { + ResolverListCommunicator resolverListCommunicator, + UserHandle initialIntentsUserHandle) { super( context, payloadIntents, @@ -52,7 +53,8 @@ public class ResolverWrapperAdapter extends ResolverListAdapter { userHandle, targetIntent, resolverListCommunicator, - false); + false, + initialIntentsUserHandle); } public CountingIdlingResource getLabelIdlingResource() { diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt index a8d6f978..9ddeed84 100644 --- a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt +++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt @@ -21,10 +21,12 @@ import android.content.Context import android.content.Intent import android.content.pm.ResolveInfo import android.content.pm.ShortcutInfo +import android.os.UserHandle import android.service.chooser.ChooserTarget import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.chooser.TargetInfo import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @@ -35,6 +37,9 @@ private const val CLASS_NAME = "./MainActivity" @SmallTest class ShortcutSelectionLogicTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val packageTargets = HashMap>().apply { arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg -> // shortcuts in reverse priority order @@ -52,7 +57,7 @@ class ShortcutSelectionLogicTest { private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(3, 0), + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), "label", "extended info", Intent(), @@ -60,7 +65,7 @@ class ShortcutSelectionLogicTest { private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent(), - ResolverDataProvider.createResolveInfo(4, 0), + ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE), "label 2", "extended info 2", Intent(), diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 3bf9f1d8..4a0eb92d 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -155,6 +155,8 @@ public class UnbundledChooserActivityTest { * -------- */ + private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser(); private static final Function DEFAULT_PM = pm -> pm; private static final Function NO_APP_PREDICTION_SERVICE_PM = pm -> { @@ -445,7 +447,7 @@ public class UnbundledChooserActivityTest { onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -472,7 +474,7 @@ public class UnbundledChooserActivityTest { List infosToStack = new ArrayList<>(); for (int i = 0; i < 4; i++) { ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i, - UserHandle.USER_CURRENT); + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); resolveInfo.activityInfo.applicationInfo.name = appName; resolveInfo.activityInfo.applicationInfo.packageName = packageName; resolveInfo.activityInfo.packageName = packageName; @@ -491,7 +493,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(6)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -522,17 +524,21 @@ public class UnbundledChooserActivityTest { verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) .topK(any(List.class), anyInt()); assertThat(activity.getIsSelected(), is(false)); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { return true; }; ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0); + DisplayResolveInfo testDri = + activity.createTestDisplayResolveInfo(sendIntent, toChoose, "testLabel", "testInfo", + sendIntent, /* resolveInfoPresentationGetter */ null); onView(withText(toChoose.activityInfo.name)) .perform(click()); waitForIdle(); verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) - .updateChooserCounts(Mockito.anyString(), anyInt(), Mockito.anyString()); + .updateChooserCounts(Mockito.anyString(), any(UserHandle.class), + Mockito.anyString()); verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1)) - .updateModel(toChoose.activityInfo.getComponentName()); + .updateModel(testDri); assertThat(activity.getIsSelected(), is(true)); } @@ -560,7 +566,7 @@ public class UnbundledChooserActivityTest { @Test public void autoLaunchSingleResult() throws InterruptedException { ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -595,7 +601,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(1)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -630,7 +636,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -661,7 +667,7 @@ public class UnbundledChooserActivityTest { assertThat(activity.getAdapter().getCount(), is(2)); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -690,10 +696,10 @@ public class UnbundledChooserActivityTest { List resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - new Intent("VIEW_TEXT")) + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -707,7 +713,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); AtomicReference launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -734,10 +740,10 @@ public class UnbundledChooserActivityTest { List resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - new Intent("VIEW_TEXT")) + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -754,7 +760,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); AtomicReference launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -784,10 +790,10 @@ public class UnbundledChooserActivityTest { List resolvedComponentInfos = Arrays.asList( ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.imageviewer", "ImageTarget"), - sendIntent), + sendIntent, PERSONAL_USER_HANDLE), ResolverDataProvider.createResolvedComponentInfo( new ComponentName("org.textviewer", "UriTarget"), - alternativeIntent) + alternativeIntent, PERSONAL_USER_HANDLE) ); setupResolverControllers(resolvedComponentInfos); @@ -801,7 +807,7 @@ public class UnbundledChooserActivityTest { waitForIdle(); AtomicReference launchedIntentRef = new AtomicReference<>(); - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { launchedIntentRef.set(targetInfo.getTargetIntent()); return true; }; @@ -1271,8 +1277,12 @@ public class UnbundledChooserActivityTest { waitForIdle(); final DisplayResolveInfo testDri = - activity.createTestDisplayResolveInfo(sendIntent, - ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent, + activity.createTestDisplayResolveInfo( + sendIntent, + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), + "testLabel", + "testInfo", + sendIntent, /* resolveInfoPresentationGetter */ null); final ChooserListAdapter adapter = activity.getAdapter(); @@ -1810,7 +1820,7 @@ public class UnbundledChooserActivityTest { // Create direct share target List serviceTargets = createDirectShareTargets(1, resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName); - ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0); + ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE); // Start activity final IChooserWrapper wrapper = (IChooserWrapper) @@ -1943,7 +1953,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendTextIntent(); sendIntent.setType(TEST_MIME_TYPE); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2099,7 +2109,7 @@ public class UnbundledChooserActivityTest { onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist()); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2326,7 +2336,7 @@ public class UnbundledChooserActivityTest { setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); Intent sendIntent = createSendTextIntent(); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2345,7 +2355,7 @@ public class UnbundledChooserActivityTest { Intent chooserIntent = createChooserIntent(createSendTextIntent(), new Intent[] {new Intent("action.fake")}); ResolveInfo[] chosen = new ResolveInfo[1]; - ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> { + ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> { chosen[0] = targetInfo.getResolveInfo(); return true; }; @@ -2473,7 +2483,7 @@ public class UnbundledChooserActivityTest { new Intent[] {new Intent("action.fake")}); ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class); ResolveInfo ri = ResolverDataProvider.createResolveInfo(0, - UserHandle.USER_CURRENT); + UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE); when( ChooserActivityOverrideData .getInstance() @@ -2523,6 +2533,46 @@ public class UnbundledChooserActivityTest { verify(workProfileShortcutLoader, times(1)).queryShortcuts(any()); } + @Test + public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() { + // enable cloneProfile + markCloneProfileUserAvailable(); + List resolvedComponentInfos = + createResolvedComponentsWithCloneProfileForTest( + 3, + PERSONAL_USER_HANDLE, + ChooserActivityOverrideData.getInstance().cloneProfileUserHandle); + setupResolverControllers(resolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + + final IChooserWrapper activity = (IChooserWrapper) mActivityRule + .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest")); + waitForIdle(); + + assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE)); + assertThat(activity.getAdapter().getCount(), is(3)); + } + + @Test + public void testClonedProfilePresent_personalTabUsesExpectedAdapter() { + markWorkProfileUserAvailable(); + markCloneProfileUserAvailable(); + List personalResolvedComponentInfos = + createResolvedComponentsForTest(3); + List workResolvedComponentInfos = createResolvedComponentsForTest( + 4); + setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); + Intent sendIntent = createSendTextIntent(); + sendIntent.setType(TEST_MIME_TYPE); + + + final IChooserWrapper activity = (IChooserWrapper) + mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test")); + waitForIdle(); + + assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE)); + } + private Intent createChooserIntent(Intent intent, Intent[] initialIntents) { Intent chooserIntent = new Intent(); chooserIntent.setAction(Intent.ACTION_CHOOSER); @@ -2557,6 +2607,7 @@ public class UnbundledChooserActivityTest { ri.activityInfo.packageName = "fake.package.name"; ri.activityInfo.applicationInfo = new ApplicationInfo(); ri.activityInfo.applicationInfo.packageName = "fake.package.name"; + ri.userHandle = UserHandle.CURRENT; return ri; } @@ -2618,7 +2669,23 @@ public class UnbundledChooserActivityTest { private List createResolvedComponentsForTest(int numberOfResults) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE)); + } + return infoList; + } + + private List createResolvedComponentsWithCloneProfileForTest( + int numberOfResults, + UserHandle resolvedForPersonalUser, + UserHandle resolvedForClonedUser) { + List infoList = new ArrayList<>(numberOfResults); + for (int i = 0; i < 1; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForPersonalUser)); + } + for (int i = 1; i < numberOfResults; i++) { + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + resolvedForClonedUser)); } return infoList; } @@ -2628,9 +2695,11 @@ public class UnbundledChooserActivityTest { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { if (i == 0) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, + PERSONAL_USER_HANDLE)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); } } return infoList; @@ -2642,9 +2711,11 @@ public class UnbundledChooserActivityTest { for (int i = 0; i < numberOfResults; i++) { if (i == 0) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); } else { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, + PERSONAL_USER_HANDLE)); } } return infoList; @@ -2654,7 +2725,8 @@ public class UnbundledChooserActivityTest { int numberOfResults, int userId) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId, + PERSONAL_USER_HANDLE)); } return infoList; } @@ -2733,6 +2805,10 @@ public class UnbundledChooserActivityTest { ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); } + private void markCloneProfileUserAvailable() { + ChooserActivityOverrideData.getInstance().cloneProfileUserHandle = UserHandle.of(11); + } + private void setupResolverControllers( List personalResolvedComponentInfos) { setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>()); diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java index 6c1edfbc..92bccb7d 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java @@ -98,7 +98,7 @@ public class UnbundledChooserActivityWorkProfileTest { public void testBlocker() { setUpPersonalAndWorkComponentInfos(); sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents(); - sOverrides.myUserId = mTestCase.getMyUserHandle().getIdentifier(); + sOverrides.tabOwnerUserHandleForLaunch = mTestCase.getMyUserHandle(); launchActivity(mTestCase.getIsSendAction()); switchToTab(mTestCase.getTab()); @@ -241,19 +241,21 @@ public class UnbundledChooserActivityWorkProfileTest { } private List createResolvedComponentsForTestWithOtherProfile( - int numberOfResults, int userId) { + int numberOfResults, int userId, UserHandle resolvedForUser) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { infoList.add( - ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId)); + ResolverDataProvider + .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser)); } return infoList; } - private List createResolvedComponentsForTest(int numberOfResults) { + private List createResolvedComponentsForTest(int numberOfResults, + UserHandle resolvedForUser) { List infoList = new ArrayList<>(numberOfResults); for (int i = 0; i < numberOfResults; i++) { - infoList.add(ResolverDataProvider.createResolvedComponentInfo(i)); + infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser)); } return infoList; } @@ -263,9 +265,9 @@ public class UnbundledChooserActivityWorkProfileTest { int workProfileTargets = 4; List personalResolvedComponentInfos = createResolvedComponentsForTestWithOtherProfile(3, - /* userId */ WORK_USER_HANDLE.getIdentifier()); + /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE); List workResolvedComponentInfos = - createResolvedComponentsForTest(workProfileTargets); + createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE); setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos); } diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt index e9c755d3..cebccaae 100644 --- a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt +++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt @@ -30,14 +30,18 @@ import com.android.intentresolver.ResolverActivity import com.android.intentresolver.ResolverDataProvider import com.google.common.truth.Truth.assertThat import org.junit.Test +import androidx.test.platform.app.InstrumentationRegistry class ImmutableTargetInfoTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val resolvedIntent = Intent("resolved") private val targetIntent = Intent("target") private val referrerFillInIntent = Intent("referrer_fillin") private val resolvedComponentName = ComponentName("resolved", "component") private val chooserTargetComponentName = ComponentName("chooser", "target") - private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0) + private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE) private val displayLabel: CharSequence = "Display Label" private val extendedInfo: CharSequence = "Extended Info" private val displayIconHolder: TargetInfo.IconHolder = mock() @@ -45,14 +49,14 @@ class ImmutableTargetInfoTest { private val sourceIntent2 = Intent("source2") private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo( Intent("display1"), - ResolverDataProvider.createResolveInfo(2, 0), + ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE), "display1 label", "display1 extended info", Intent("display1_resolved"), /* resolveInfoPresentationGetter= */ null) private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo( Intent("display2"), - ResolverDataProvider.createResolveInfo(3, 0), + ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE), "display2 label", "display2 extended info", Intent("display2_resolved"), @@ -66,7 +70,7 @@ class ImmutableTargetInfoTest { UserHandle.CURRENT) private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo( Intent("displayresolve"), - ResolverDataProvider.createResolveInfo(5, 0), + ResolverDataProvider.createResolveInfo(5, 0, PERSONAL_USER_HANDLE), "displayresolve label", "displayresolve extended info", Intent("display_resolved"), diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt index dddbcccb..886e32df 100644 --- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt +++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt @@ -41,6 +41,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify class TargetInfoTest { + private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry + .getInstrumentation().getTargetContext().getUser() + private val context = InstrumentationRegistry.getInstrumentation().getContext() @Before @@ -81,7 +84,7 @@ class TargetInfoTest { val resolvedIntent = Intent() val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo( resolvedIntent, - ResolverDataProvider.createResolveInfo(1, 0), + ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE), "label", "extended info", resolvedIntent, @@ -190,7 +193,7 @@ class TargetInfoTest { intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") intent.setType("text/plain") - val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) val targetInfo = DisplayResolveInfo.newDisplayResolveInfo( intent, @@ -268,7 +271,7 @@ class TargetInfoTest { intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending") intent.setType("text/plain") - val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0) + val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE) val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo( intent, resolveInfo, diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java index 006f3b2d..892a2e28 100644 --- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java +++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java @@ -28,6 +28,9 @@ import android.os.Message; import androidx.test.InstrumentationRegistry; import com.android.intentresolver.ResolvedComponentInfo; +import com.android.intentresolver.chooser.TargetInfo; + +import com.google.android.collect.Lists; import org.junit.Test; @@ -81,7 +84,8 @@ public class AbstractResolverComparatorTest { Intent intent = new Intent(); AbstractResolverComparator testComparator = - new AbstractResolverComparator(context, intent) { + new AbstractResolverComparator(context, intent, + Lists.newArrayList(context.getUser())) { @Override int compare(ResolveInfo lhs, ResolveInfo rhs) { @@ -94,7 +98,7 @@ public class AbstractResolverComparatorTest { void doCompute(List targets) {} @Override - public float getScore(ComponentName name) { + public float getScore(TargetInfo targetInfo) { return 0; } -- cgit v1.2.3-59-g8ed1b From cfcebe11339f43a261bb7873ed7ee49909042e28 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Wed, 15 Feb 2023 13:25:32 -0800 Subject: Update Chooser icon animation; animate labels Legacy Chooser animation had a few issues that resulted in icons sometimes blinking when they were animting into the view: * The target resolution logic causes multiple adapter updates, when an updated is happending during an animation, RecyclerView may bind the animated view to a different item interrupting the animation. To transfer the animation state between view animatin progress (alpha) is now stores in the adapter; * OnbjectAnimator runs regardles of the view's state (whether is ready to be drawn); it is replaced with the view animation. * ChooserListAdapter did not fully reset its state on #rebuildList() call. Bug: 262927266 Test: manual testing Change-Id: I73e1c1596996d35bdf00ce5a7e3dd331f4e39f60 --- .../android/intentresolver/ChooserListAdapter.java | 23 ++++++- .../intentresolver/ItemRevealAnimationTracker.kt | 70 ++++++++++++++++++++++ .../intentresolver/ResolverListAdapter.java | 18 +----- 3 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java index dab44577..ceeed6f0 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -98,6 +98,8 @@ public class ChooserListAdapter extends ResolverListAdapter { // Sorted list of DisplayResolveInfos for the alphabetical app section. private List mSortedList = new ArrayList<>(); + private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker(); + // For pinned direct share labels, if the text spans multiple lines, the TextView will consume // the full width, even if the characters actually take up less than that. Measure the actual // line widths and constrain the View's width based upon that so that the pin doesn't end up @@ -243,6 +245,15 @@ public class ChooserListAdapter extends ResolverListAdapter { } + @Override + protected boolean rebuildList(boolean doPostProcessing) { + mAnimationTracker.reset(); + mSortedList.clear(); + boolean result = super.rebuildList(doPostProcessing); + notifyDataSetChanged(); + return result; + } + private void createPlaceHolders() { mServiceTargets.clear(); for (int i = 0; i < mMaxRankedTargets; ++i) { @@ -266,7 +277,17 @@ public class ChooserListAdapter extends ResolverListAdapter { } holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel()); - holder.bindIcon(info, /*animate =*/ true); + mAnimationTracker.animateLabel(holder.text, info); + if (holder.text2.getVisibility() == View.VISIBLE) { + mAnimationTracker.animateLabel(holder.text2, info); + } + holder.bindIcon(info); + if (info.getDisplayIconHolder().getDisplayIcon() != null) { + mAnimationTracker.animateIcon(holder.icon, info); + } else { + holder.icon.clearAnimation(); + } + if (info.isSelectableTargetInfo()) { // direct share targets should append the application name for a better readout DisplayResolveInfo rInfo = info.getDisplayResolveInfo(); diff --git a/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt b/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt new file mode 100644 index 00000000..d3e07c6b --- /dev/null +++ b/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt @@ -0,0 +1,70 @@ +/* + * 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 + +import android.view.View +import android.view.animation.AlphaAnimation +import android.view.animation.LinearInterpolator +import android.view.animation.Transformation +import com.android.intentresolver.chooser.TargetInfo + +private const val IMAGE_FADE_IN_MILLIS = 150L + +internal class ItemRevealAnimationTracker { + private val iconProgress = HashMap() + private val labelProgress = HashMap() + + fun reset() { + iconProgress.clear() + labelProgress.clear() + } + + fun animateIcon(view: View, info: TargetInfo) = animateView(view, info, iconProgress) + fun animateLabel(view: View, info: TargetInfo) = animateView(view, info, labelProgress) + + private fun animateView(view: View, info: TargetInfo, map: MutableMap) { + val record = map.getOrPut(info) { + Record() + } + if ((view.animation as? RevealAnimation)?.record === record) return + + view.clearAnimation() + if (record.alpha >= 1f) { + view.alpha = 1f + return + } + + view.startAnimation(RevealAnimation(record)) + } + + private class Record(var alpha: Float = 0f) + + private class RevealAnimation(val record: Record) : AlphaAnimation(record.alpha, 1f) { + init { + duration = (IMAGE_FADE_IN_MILLIS * (1f - record.alpha)).toLong() + interpolator = LinearInterpolator() + } + + override fun applyTransformation(interpolatedTime: Float, t: Transformation) { + super.applyTransformation(interpolatedTime, t) + // One TargetInfo can be simultaneously bou into multiple UI grid items; make sure + // that the alpha value only increases. This should not affect running animations, only + // a starting point for a new animation when a different view is bound to this target. + record.alpha = minOf(1f, maxOf(record.alpha, t.alpha)) + } + } +} diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java index b0586f2d..f090f3a2 100644 --- a/java/src/com/android/intentresolver/ResolverListAdapter.java +++ b/java/src/com/android/intentresolver/ResolverListAdapter.java @@ -18,7 +18,6 @@ package com.android.intentresolver; import static android.content.Context.ACTIVITY_SERVICE; -import android.animation.ObjectAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -43,7 +42,6 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.animation.DecelerateInterpolator; import android.widget.AbsListView; import android.widget.BaseAdapter; import android.widget.ImageView; @@ -926,7 +924,6 @@ public class ResolverListAdapter extends BaseAdapter { */ @VisibleForTesting public static class ViewHolder { - private static final long IMAGE_FADE_IN_MILLIS = 150; public View itemView; public Drawable defaultItemViewBackground; @@ -964,23 +961,12 @@ public class ResolverListAdapter extends BaseAdapter { itemView.setContentDescription(description); } - public void bindIcon(TargetInfo info) { - bindIcon(info, false); - } - /** - * Bind view holder to a TargetInfo, run icon reveal animation, if required. + * Bind view holder to a TargetInfo. */ - public void bindIcon(TargetInfo info, boolean animate) { + public void bindIcon(TargetInfo info) { Drawable displayIcon = info.getDisplayIconHolder().getDisplayIcon(); - boolean runAnimation = animate && (icon.getDrawable() == null) && (displayIcon != null); icon.setImageDrawable(displayIcon); - if (runAnimation) { - ObjectAnimator animator = ObjectAnimator.ofFloat(icon, "alpha", 0.0f, 1.0f); - animator.setInterpolator(new DecelerateInterpolator(1.0f)); - animator.setDuration(IMAGE_FADE_IN_MILLIS); - animator.start(); - } if (info.isSuspended()) { icon.setColorFilter(getSuspendedColorMatrix()); } else { -- cgit v1.2.3-59-g8ed1b From 7e145c5e1c4be62c47bee591d6e96eb361c9dfdc Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Thu, 23 Mar 2023 15:02:23 +0000 Subject: Skip refinement for suspended targets. It's just rude to send users into the refinement flow when we expect the launch to fail (with a message dialog indicating that the entire target app was never supposed to work; i.e., we make it clear to users that this failure had nothing to do with their refinement choices). OTOH if we skip refinement, we have some handling to bounce back into a usable Sharesheet session after that dialog appears, so we'd like to take advantage of that. A subsequent CL will "plug all the holes" in our handling of various refinement exit conditions. There's currently an analogous post-refinement "bounce back" for suspended targets, but we won't be able to recover a Sharesheet session at that time, so (TODO) we'll turn that into a hard failure (as with any other refinement problems). Bug: 273864843 Test: manually tested `adb shell cmd package suspend ` confirm we no longer invoke refinement (and instead trigger the system dialog immediately) when the suspended target is chosen. Change-Id: Iff1cf67f67015c754437726b8f52431deca797e0 --- java/src/com/android/intentresolver/ChooserRefinementManager.java | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java index 3ddc1c7c..23cf6ba0 100644 --- a/java/src/com/android/intentresolver/ChooserRefinementManager.java +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -77,6 +77,13 @@ public final class ChooserRefinementManager { if (selectedTarget.getAllSourceIntents().isEmpty()) { return false; } + if (selectedTarget.isSuspended()) { + // We expect all launches to fail for this target, so don't make the user go through the + // refinement flow first. Besides, the default (non-refinement) handling displays a + // warning in this case and recovers the session; we won't be equipped to recover if + // problems only come up after refinement. + return false; + } destroy(); // Terminate any prior sessions. mRefinementResultReceiver = new RefinementResultReceiver( -- cgit v1.2.3-59-g8ed1b From 64f9435ffbd82cf2f025aebc05795691aa5e1e08 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Thu, 23 Mar 2023 15:54:59 +0000 Subject: Fix/clarify refinement termination conditions This primarily closes some gaps where our old implementation was willing to leave refinement flows hanging in assorted edge cases. As of this CL, every refinement flow ends in exactly one of: A) the flow is cancelled by `ChooserRefinementManager.destroy()` (e.g. we do this when `ChooserActivity` is closing); B) `ChooserRefinementManager` fires its configured success callback; C) `ChooserRefinementManager` fires its configured failure callback. After any of those outcomes, the refinement manager cleans up its own state (i.e., clients are no longer responsible for calling `destroy()` during routine handling of their callbacks), and I've noted in an implementation comment that we can use the nullability of `ChooserRefinementManager.mRefinementResultReceiver` as an indication of whether there's an active refinement session. These receivers are always destroyed at the end of the session, and can never receive more than one result callback in a single session. I've added a `@UiThread` annotation to the entire class to clarify the concurrency model, so it's easy to reason about the synchronized computation in this class. A subsequent CL will expose this "active session" condition in the refinement manager API so that `ChooserActivity` can detect the case when the refinement activity closes with no result. `ChooserActivity` also needs to stop suppressing its own termination when a refined launch fails due to the target being suspended. Then we'll be able to assert that chooser always finishes after *any* attempted refinement. Bug: 273864843 Test: manually tested with a custom refinement activity to trigger all the failure cases under `ChooserRefinementManager` responsibility. As described above, `ChooserActivity` needs to handle some others external to the refinement manager; those are punted from this CL. Change-Id: Iee399e34dec27b9a846ea040831e23fafda46fd5 --- .../android/intentresolver/ChooserActivity.java | 5 +- .../intentresolver/ChooserRefinementManager.java | 60 +++++++++++++--------- 2 files changed, 36 insertions(+), 29 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 2a73c42a..30ec2b91 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -279,10 +279,7 @@ public class ChooserActivity extends ResolverActivity implements finish(); } }, - () -> { - mRefinementManager.destroy(); - finish(); - }); + this::finish); mChooserContentPreviewUi = new ChooserContentPreviewUi( mChooserRequest.getTargetIntent(), diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java index 23cf6ba0..72b5305d 100644 --- a/java/src/com/android/intentresolver/ChooserRefinementManager.java +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -17,6 +17,7 @@ package com.android.intentresolver; import android.annotation.Nullable; +import android.annotation.UiThread; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -25,7 +26,6 @@ import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.os.Handler; import android.os.Parcel; -import android.os.Parcelable; import android.os.ResultReceiver; import android.util.Log; @@ -41,6 +41,7 @@ import java.util.function.Consumer; * convert the format of the payload, or lazy-download some data that was deferred in the original * call). */ +@UiThread public final class ChooserRefinementManager { private static final String TAG = "ChooserRefinement"; @@ -51,7 +52,7 @@ public final class ChooserRefinementManager { private final Consumer mOnSelectionRefined; private final Runnable mOnRefinementCancelled; - @Nullable + @Nullable // Non-null only during an active refinement session. private RefinementResultReceiver mRefinementResultReceiver; public ChooserRefinementManager( @@ -98,7 +99,10 @@ public final class ChooserRefinementManager { mOnRefinementCancelled.run(); } }, - mOnRefinementCancelled, + () -> { + destroy(); + mOnRefinementCancelled.run(); + }, mContext.getMainThreadHandler()); Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget); @@ -114,7 +118,7 @@ public final class ChooserRefinementManager { /** Clean up any ongoing refinement session. */ public void destroy() { if (mRefinementResultReceiver != null) { - mRefinementResultReceiver.destroy(); + mRefinementResultReceiver.destroyReceiver(); mRefinementResultReceiver = null; } } @@ -151,7 +155,7 @@ public final class ChooserRefinementManager { mOnRefinementCancelled = onRefinementCancelled; } - public void destroy() { + public void destroyReceiver() { mDestroyed = true; } @@ -161,27 +165,14 @@ public final class ChooserRefinementManager { Log.e(TAG, "Destroyed RefinementResultReceiver received a result"); return; } - if (resultData == null) { - Log.e(TAG, "RefinementResultReceiver received null resultData"); - // TODO: treat as cancellation? - return; - } - switch (resultCode) { - case Activity.RESULT_CANCELED: - mOnRefinementCancelled.run(); - break; - case Activity.RESULT_OK: - Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT); - if (intentParcelable instanceof Intent) { - mOnSelectionRefined.accept((Intent) intentParcelable); - } else { - Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data"); - } - break; - default: - Log.w(TAG, "Received unknown refinement result " + resultCode); - break; + destroyReceiver(); // This is the single callback we'll accept from this session. + + Intent refinedResult = tryToExtractRefinedResult(resultCode, resultData); + if (refinedResult == null) { + mOnRefinementCancelled.run(); + } else { + mOnSelectionRefined.accept(refinedResult); } } @@ -197,5 +188,24 @@ public final class ChooserRefinementManager { parcel.recycle(); return receiverForSending; } + + /** + * Get the refinement from the result data, if possible, or log diagnostics and return null. + */ + @Nullable + private static Intent tryToExtractRefinedResult(int resultCode, Bundle resultData) { + if (Activity.RESULT_CANCELED == resultCode) { + Log.i(TAG, "Refinement canceled by caller"); + } else if (Activity.RESULT_OK != resultCode) { + Log.w(TAG, "Canceling refinement on unrecognized result code " + resultCode); + } else if (resultData == null) { + Log.e(TAG, "RefinementResultReceiver received null resultData; canceling"); + } else if (!(resultData.getParcelable(Intent.EXTRA_INTENT) instanceof Intent)) { + Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data"); + } else { + return resultData.getParcelable(Intent.EXTRA_INTENT, Intent.class); + } + return null; + } } } -- cgit v1.2.3-59-g8ed1b From 90aab48da1198be6a154412b3cd9a44cf97b7764 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Thu, 23 Mar 2023 16:26:28 +0000 Subject: Always terminate Chooser after a refinement flow. In normal (non-refinement) target selection, if the user selects a suspended target, we attempt to launch it anyways in order to prompt a system message dialog, but we suppress our normal post-launch termination so that the user can try to select a different target. We had similar "bounce-back" logic if a post-refinement launch failed as a result of a suspended target, but the UX doesn't really make sense in the refinement case (and I'm also not 100% confident that our implementation would be technically equipped to handle the re-entry). I've removed the "bounce-back" suppression in the post-refinement case, so if we show the dialog about the package being suspended, that's the end of the session. (Although note, unless the package was suspended *during* refinement, we would've already shown the dialog instead of going to refinement in the first place.) Bug: 273864843 Test: manually tested behavior around suspended targets, and ran CTS to verify no regressions in normal refinement usage. Change-Id: I4e57add285224b7f311c4d0532168dc984ee331b --- java/src/com/android/intentresolver/ChooserActivity.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 30ec2b91..71a94e11 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -275,9 +275,13 @@ public class ChooserActivity extends ResolverActivity implements mChooserRequest.getRefinementIntentSender(), (validatedRefinedTarget) -> { maybeRemoveSharedText(validatedRefinedTarget); - if (super.onTargetSelected(validatedRefinedTarget, false)) { - finish(); - } + + // We already block suspended targets from going to refinement, and we probably + // can't recover a Chooser session if that's the reason the refined target fails + // to launch now. Fire-and-forget the refined launch; ignore the return value + // and just make sure the Sharesheet session gets cleaned up regardless. + super.onTargetSelected(validatedRefinedTarget, false); + finish(); }, this::finish); -- cgit v1.2.3-59-g8ed1b From aec171251864334d43c3fd14fe10f8bc9f45b535 Mon Sep 17 00:00:00 2001 From: Joshua Trask Date: Thu, 23 Mar 2023 17:05:01 +0000 Subject: Terminate refinement on back-out w/ no result Bug: 273864843 Test: manually tested back-out behavior with a custom refinement activity that calls `finish()` on launch. Ran CTS & `IntentResolverUnitTests` to confirm no regressions in the normal refinement flow. (Also noted that this condition *isn't* triggered when we finish after sending a valid RESULT_OK refinement. That's not really required for correctness, but it's still simpler/preferable that way, so it was a relief to confirm.) Change-Id: I165c958e3633430006b7977faa7617a3f87b1092 --- java/src/com/android/intentresolver/ChooserActivity.java | 9 +++++++++ .../com/android/intentresolver/ChooserRefinementManager.java | 10 ++++++++++ 2 files changed, 19 insertions(+) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 71a94e11..34278065 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -620,6 +620,15 @@ public class ChooserActivity extends ResolverActivity implements super.onResume(); Log.d(TAG, "onResume: " + getComponentName().flattenToShortString()); maybeCancelFinishAnimation(); + + if (mRefinementManager.isAwaitingRefinementResult()) { + // This can happen if the refinement activity terminates without ever sending a response + // to our `ResultReceiver`. We're probably not prepared to return the user into a valid + // Chooser session, so we'll treat it as a cancellation instead. + Log.w(TAG, "Chooser resumed while awaiting refinement result; aborting"); + mRefinementManager.destroy(); + finish(); + } } @Override diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java index 72b5305d..8d7b1aac 100644 --- a/java/src/com/android/intentresolver/ChooserRefinementManager.java +++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java @@ -66,6 +66,16 @@ public final class ChooserRefinementManager { mOnRefinementCancelled = onRefinementCancelled; } + /** + * @return whether a refinement session has been initiated (i.e., an earlier call to + * {@link #maybeHandleSelection(TargetInfo)} returned true), and isn't yet complete. The session + * is complete if the refinement activity calls {@link ResultReceiver#onResultReceived()} (with + * any result), or if it's cancelled on our side by {@link ChooserRefinementManager#destroy()}. + */ + public boolean isAwaitingRefinementResult() { + return (mRefinementResultReceiver != null); + } + /** * Delegate the user's {@code selectedTarget} to the refinement flow, if possible. * @return true if the selection should wait for a now-started refinement flow, or false if it -- cgit v1.2.3-59-g8ed1b From bf2977684f6518a140e44035e01b8873a4c76453 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Sun, 26 Mar 2023 23:06:37 -0700 Subject: Fallback to ShortcutManager if AppPredictor has crashed Bug: 269230501 Test: unit tests Change-Id: If5ac83d0f301bda8d00721f56ed8df88332a147f --- .../intentresolver/shortcuts/ShortcutLoader.kt | 11 +++- .../intentresolver/shortcuts/ShortcutLoaderTest.kt | 75 ++++++++++++++++------ 2 files changed, 65 insertions(+), 21 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt index 6f7542f1..29e706d4 100644 --- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt +++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt @@ -77,6 +77,7 @@ open class ShortcutLoader @VisibleForTesting constructor( 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 @MainThread @@ -134,8 +135,14 @@ open class ShortcutLoader @VisibleForTesting constructor( @WorkerThread private fun queryDirectShareTargets(skipAppPredictionService: Boolean) { if (!skipAppPredictionService && appPredictor != null) { - appPredictor.requestPredictionUpdate() - return + try { + appPredictor.requestPredictionUpdate() + return + } catch (e: Throwable) { + // we might have been destroyed concurrently, nothing left to do + if (isDestroyed) return + Log.e(TAG, "Failed to query AppPredictor", e) + } } // Default to just querying ShortcutManager if AppPredictor not present. if (targetIntentFilter == null) return diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index 0c817cb2..e8e2f862 100644 --- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -58,7 +58,7 @@ class ShortcutLoaderTest { private val pm = mock { whenever(getApplicationInfo(any(), any())).thenReturn(appInfo) } - val userManager = mock { + private val userManager = mock { whenever(isUserRunning(any())).thenReturn(true) whenever(isUserUnlocked(any())).thenReturn(true) whenever(isQuietModeEnabled(any())).thenReturn(false) @@ -72,14 +72,15 @@ class ShortcutLoaderTest { private val intentFilter = mock() private val appPredictor = mock() private val callback = mock>() + private val componentName = ComponentName("pkg", "Class") + private val appTarget = mock { + whenever(resolvedComponentName).thenReturn(componentName) + } + private val appTargets = arrayOf(appTarget) + private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) @Test fun test_queryShortcuts_result_consistency_with_AppPredictor() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) val testSubject = ShortcutLoader( context, appPredictor, @@ -93,7 +94,6 @@ class ShortcutLoaderTest { testSubject.queryShortcuts(appTargets) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val matchingAppTarget = createAppTarget(matchingShortcutInfo) val shortcuts = listOf( matchingAppTarget, @@ -131,12 +131,6 @@ class ShortcutLoaderTest { @Test fun test_queryShortcuts_result_consistency_with_ShortcutManager() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -182,12 +176,6 @@ class ShortcutLoaderTest { @Test fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() { - val componentName = ComponentName("pkg", "Class") - val appTarget = mock { - whenever(resolvedComponentName).thenReturn(componentName) - } - val appTargets = arrayOf(appTarget) - val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1) val shortcutManagerResult = listOf( ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), // mismatching shortcut @@ -237,6 +225,55 @@ class ShortcutLoaderTest { } } + @Test + fun test_queryShortcuts_onAppPredictorFailure_fallbackToShortcutManager() { + val shortcutManagerResult = listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = mock { + whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult) + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + whenever(appPredictor.requestPredictionUpdate()) + .thenThrow(IllegalStateException("Test exception")) + val testSubject = ShortcutLoader( + context, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + executor, + executor, + callback + ) + + testSubject.queryShortcuts(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + + val resultCaptor = argumentCaptor() + verify(callback, times(1)).accept(capture(resultCaptor)) + + val result = resultCaptor.value + assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor) + assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets) + assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size) + assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertTrue( + "AppTargets are not expected the cache of a ShortcutManager result", + result.directShareAppTargetCache.isEmpty() + ) + assertEquals( + "Wrong ShortcutInfo in the cache", + matchingShortcutInfo, + result.directShareShortcutInfoCache[shortcut] + ) + } + } + @Test fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() { testDisabledWorkProfileDoNotCallSystem(isUserRunning = false) -- cgit v1.2.3-59-g8ed1b From 4c421ccd6e61636dca7fddc58f0f7205d092623a Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 27 Mar 2023 17:57:42 -0700 Subject: Print special logcat warning SecurityException If a SecurityException is thrown while trying to read data for a provided URI, print a special warning suggesting granting URI permissions. Bug: 273890881 Test: use test application to check that the warning is printed Change-Id: I7de6b8bade1c90943bc105e4648f12daa3b6580f --- .../contentpreview/ChooserContentPreviewUi.java | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 6892b32c..3509c67d 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -369,30 +369,36 @@ public final class ChooserContentPreviewUi { private static String getType(ContentInterface resolver, Uri uri) { try { return resolver.getType(uri); + } catch (SecurityException e) { + logProviderPermissionWarning(uri, "mime type"); } catch (Throwable t) { Log.e(ContentPreviewUi.TAG, "Failed to read content type, uri: " + uri, t); - return null; } + return null; } @Nullable private static Cursor query(ContentInterface resolver, Uri uri) { try { return resolver.query(uri, null, null, null); + } catch (SecurityException e) { + logProviderPermissionWarning(uri, "metadata"); } catch (Throwable t) { Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: " + uri, t); - return null; } + return null; } @Nullable private static String[] getStreamTypes(ContentInterface resolver, Uri uri) { try { return resolver.getStreamTypes(uri, "*/*"); + } catch (SecurityException e) { + logProviderPermissionWarning(uri, "stream types"); } catch (Throwable t) { Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: " + uri, t); - return null; } + return null; } private static String getFileName(Uri uri) { @@ -404,4 +410,11 @@ public final class ChooserContentPreviewUi { } return fileName; } + + private static void logProviderPermissionWarning(Uri uri, String dataName) { + // The ContentResolver already logs the exception. Log something more informative. + Log.w(ContentPreviewUi.TAG, "Could not read " + uri + " " + dataName + "." + + " If a preview is desired, call Intent#setClipData() to ensure that the" + + " sharesheet is given permission."); + } } -- cgit v1.2.3-59-g8ed1b From df5ea9d3683947c09c53c632d1e4822b31b88a88 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 28 Mar 2023 15:28:00 +0000 Subject: Hide all system actions if there are custom actions. Nearby will have a separate home for launch, the others are intentionally hidden per product feedback. Bug: 274645844 Test: atest ContentPreviewUiTest Test: atest CtsSharesheetDeviceTest Change-Id: I5cd9157bc3ab6fd38a9993e470604633fd41c7e2 --- .../contentpreview/ContentPreviewUi.java | 6 +- .../contentpreview/ContentPreviewUiTest.kt | 80 ++++++++++++++++++++++ 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index a6bc2164..2a6bff5c 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -75,9 +75,11 @@ abstract class ContentPreviewUi { FeatureFlagRepository featureFlagRepository) { ArrayList actions = new ArrayList<>(systemActions.size() + customActions.size()); - actions.addAll(systemActions); - if (featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)) { + if (featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS) + && customActions != null && !customActions.isEmpty()) { actions.addAll(customActions); + } else { + actions.addAll(systemActions); } return actions; } diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt new file mode 100644 index 00000000..2b78a262 --- /dev/null +++ b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt @@ -0,0 +1,80 @@ +/* + * 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.contentpreview + +import android.content.res.Resources +import android.view.LayoutInflater +import android.view.ViewGroup +import com.android.intentresolver.TestFeatureFlagRepository +import com.android.intentresolver.flags.FeatureFlagRepository +import com.android.intentresolver.flags.Flags +import com.android.intentresolver.widget.ActionRow +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ContentPreviewUiTest { + private class TestablePreview(private val flags: FeatureFlagRepository) : ContentPreviewUi() { + override fun getType() = 0 + + override fun display( + resources: Resources?, + layoutInflater: LayoutInflater?, + parent: ViewGroup? + ): ViewGroup { + throw IllegalStateException() + } + + // exposing for testing + fun makeActions( + system: List, + custom: List + ): List { + return createActions(system, custom, flags) + } + } + + @Test + fun testCreateActions() { + val featureFlagRepository = TestFeatureFlagRepository( + mapOf( + Flags.SHARESHEET_CUSTOM_ACTIONS to true + ) + ) + val preview = TestablePreview(featureFlagRepository) + + val system = listOf(ActionRow.Action(label="system", icon=null) {}) + val custom = listOf(ActionRow.Action(label="custom", icon=null) {}) + + assertThat(preview.makeActions(system, custom)).isEqualTo(custom) + assertThat(preview.makeActions(system, listOf())).isEqualTo(system) + } + + @Test + fun testCreateActions_flagDisabled() { + val featureFlagRepository = TestFeatureFlagRepository( + mapOf( + Flags.SHARESHEET_CUSTOM_ACTIONS to false + ) + ) + val preview = TestablePreview(featureFlagRepository) + + val system = listOf(ActionRow.Action(label="system", icon=null) {}) + val custom = listOf(ActionRow.Action(label="custom", icon=null) {}) + + assertThat(preview.makeActions(system, custom)).isEqualTo(system) + } +} -- cgit v1.2.3-59-g8ed1b From 7a9decf0189c8674d2580cc9a117eebfeee0a9d2 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 4 Apr 2023 16:28:03 +0000 Subject: Remove SHARESHEET_CUSTOM_ACTIONS flag usage Has been released for a while without issue. Everyone loves it. Bug: 266983432 Test: atest IntentResolverUnitTests Change-Id: I8fdf7014415d53df2dabd55ec7bbdfb1426e093e --- java/res/layout/chooser_action_row.xml | 32 ---------------------- .../intentresolver/ChooserRequestParameters.java | 4 +-- .../contentpreview/ContentPreviewUi.java | 12 +++----- .../contentpreview/FileContentPreviewUi.java | 5 ++-- .../contentpreview/ImageContentPreviewUi.java | 5 ++-- .../contentpreview/TextContentPreviewUi.java | 5 ++-- .../contentpreview/UnifiedContentPreviewUi.java | 5 ++-- java/src/com/android/intentresolver/flags/Flags.kt | 5 ---- .../UnbundledChooserActivityTest.java | 4 --- .../contentpreview/ContentPreviewUiTest.kt | 29 ++------------------ 10 files changed, 16 insertions(+), 90 deletions(-) delete mode 100644 java/res/layout/chooser_action_row.xml (limited to 'java/src') diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml deleted file mode 100644 index 620ff704..00000000 --- a/java/res/layout/chooser_action_row.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index 8e0014d6..d11561df 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -146,9 +146,7 @@ public class ChooserRequestParameters { mTargetIntentFilter = getTargetIntentFilter(mTarget); - mChooserActions = featureFlags.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS) - ? getChooserActions(clientIntent) - : ImmutableList.of(); + mChooserActions = getChooserActions(clientIntent); mModifyShareAction = featureFlags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) ? getModifyShareAction(clientIntent) : null; diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 2a6bff5c..05d0ee66 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -54,10 +54,8 @@ abstract class ContentPreviewUi { public abstract ViewGroup display( Resources resources, LayoutInflater layoutInflater, ViewGroup parent); - protected static int getActionRowLayout(FeatureFlagRepository featureFlagRepository) { - return featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS) - ? R.layout.scrollable_chooser_action_row - : R.layout.chooser_action_row; + protected static int getActionRowLayout() { + return R.layout.scrollable_chooser_action_row; } protected static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) { @@ -71,12 +69,10 @@ abstract class ContentPreviewUi { protected static List createActions( List systemActions, - List customActions, - FeatureFlagRepository featureFlagRepository) { + List customActions) { ArrayList actions = new ArrayList<>(systemActions.size() + customActions.size()); - if (featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS) - && customActions != null && !customActions.isEmpty()) { + if (customActions != null && !customActions.isEmpty()) { actions.addAll(customActions); } else { actions.addAll(systemActions); diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 52e20cf0..cc087a63 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -74,7 +74,7 @@ class FileContentPreviewUi extends ContentPreviewUi { private ViewGroup displayInternal( Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); + @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_file, parent, false); @@ -119,8 +119,7 @@ class FileContentPreviewUi extends ContentPreviewUi { actionRow.setActions( createActions( createFilePreviewActions(), - mActionFactory.createCustomActions(), - mFeatureFlagRepository)); + mActionFactory.createCustomActions())); } return contentPreviewLayout; diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index f2c0564a..1ca2ba61 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -88,7 +88,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { } private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); + @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); ChooserImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); @@ -98,8 +98,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { actionRow.setActions( createActions( createImagePreviewActions(), - mActionFactory.createCustomActions(), - mFeatureFlagRepository)); + mActionFactory.createCustomActions())); } if (mImageUris.size() == 0) { diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index d0cba5bb..8a3c2259 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -80,7 +80,7 @@ class TextContentPreviewUi extends ContentPreviewUi { private ViewGroup displayInternal( LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); + @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_text, parent, false); @@ -89,8 +89,7 @@ class TextContentPreviewUi extends ContentPreviewUi { actionRow.setActions( createActions( createTextPreviewActions(), - mActionFactory.createCustomActions(), - mFeatureFlagRepository)); + mActionFactory.createCustomActions())); } if (mSharingText == null) { diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 2d2ae52b..dfc171d2 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -94,7 +94,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { } private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository); + @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); ScrollableImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); @@ -104,8 +104,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { actionRow.setActions( createActions( createImagePreviewActions(), - mActionFactory.createCustomActions(), - mFeatureFlagRepository)); + mActionFactory.createCustomActions())); } if (mFiles.size() == 0) { diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 40f32bf3..846f8e64 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -22,15 +22,10 @@ import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to // make the flags available in the flag flipper app (see go/sysui-flags). object Flags { - const val SHARESHEET_CUSTOM_ACTIONS_NAME = "sharesheet_custom_actions" const val SHARESHEET_RESELECTION_ACTION_NAME = "sharesheet_reselection_action" const val SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME = "sharesheet_image_text_preview" const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview" - // TODO(b/266983432) Tracking Bug - @JvmField - val SHARESHEET_CUSTOM_ACTIONS = releasedFlag(1501, SHARESHEET_CUSTOM_ACTIONS_NAME) - // TODO(b/266982749) Tracking Bug @JvmField val SHARESHEET_RESELECTION_ACTION = releasedFlag(1502, SHARESHEET_RESELECTION_ACTION_NAME) diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 6744d625..088e8923 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -169,7 +169,6 @@ public class UnbundledChooserActivityTest { private static final List ALL_FLAGS = Arrays.asList( - Flags.SHARESHEET_CUSTOM_ACTIONS, Flags.SHARESHEET_RESELECTION_ACTION, Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); @@ -1719,9 +1718,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_CUSTOM_ACTIONS_NAME }, - values = { true }) public void testLaunchWithCustomAction() throws InterruptedException { List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt index 2b78a262..c6a47515 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt @@ -19,15 +19,12 @@ package com.android.intentresolver.contentpreview import android.content.res.Resources import android.view.LayoutInflater import android.view.ViewGroup -import com.android.intentresolver.TestFeatureFlagRepository -import com.android.intentresolver.flags.FeatureFlagRepository -import com.android.intentresolver.flags.Flags import com.android.intentresolver.widget.ActionRow import com.google.common.truth.Truth.assertThat import org.junit.Test class ContentPreviewUiTest { - private class TestablePreview(private val flags: FeatureFlagRepository) : ContentPreviewUi() { + private class TestablePreview() : ContentPreviewUi() { override fun getType() = 0 override fun display( @@ -43,18 +40,13 @@ class ContentPreviewUiTest { system: List, custom: List ): List { - return createActions(system, custom, flags) + return createActions(system, custom) } } @Test fun testCreateActions() { - val featureFlagRepository = TestFeatureFlagRepository( - mapOf( - Flags.SHARESHEET_CUSTOM_ACTIONS to true - ) - ) - val preview = TestablePreview(featureFlagRepository) + val preview = TestablePreview() val system = listOf(ActionRow.Action(label="system", icon=null) {}) val custom = listOf(ActionRow.Action(label="custom", icon=null) {}) @@ -62,19 +54,4 @@ class ContentPreviewUiTest { assertThat(preview.makeActions(system, custom)).isEqualTo(custom) assertThat(preview.makeActions(system, listOf())).isEqualTo(system) } - - @Test - fun testCreateActions_flagDisabled() { - val featureFlagRepository = TestFeatureFlagRepository( - mapOf( - Flags.SHARESHEET_CUSTOM_ACTIONS to false - ) - ) - val preview = TestablePreview(featureFlagRepository) - - val system = listOf(ActionRow.Action(label="system", icon=null) {}) - val custom = listOf(ActionRow.Action(label="custom", icon=null) {}) - - assertThat(preview.makeActions(system, custom)).isEqualTo(system) - } } -- cgit v1.2.3-59-g8ed1b From 0a8bcadca1a463d8854e944fbb6cc96c08828bf6 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 4 Apr 2023 19:58:31 +0000 Subject: Remove flag SHARESHEET_RESELECTION_ACTION Bug: 266982749 Test: Build and run with modify share. Change-Id: Id36a84c82e05c9fc946543c3eaa1271f786a5068 --- java/src/com/android/intentresolver/ChooserActionFactory.java | 5 +---- .../src/com/android/intentresolver/ChooserRequestParameters.java | 5 +---- .../android/intentresolver/contentpreview/ContentPreviewUi.java | 8 ++------ .../intentresolver/contentpreview/FileContentPreviewUi.java | 2 +- .../intentresolver/contentpreview/ImageContentPreviewUi.java | 2 +- .../intentresolver/contentpreview/TextContentPreviewUi.java | 2 +- .../intentresolver/contentpreview/UnifiedContentPreviewUi.java | 2 +- java/src/com/android/intentresolver/flags/Flags.kt | 5 ----- .../src/com/android/intentresolver/ChooserActionFactoryTest.kt | 9 --------- .../com/android/intentresolver/UnbundledChooserActivityTest.java | 4 ---- 10 files changed, 8 insertions(+), 36 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index d0e6e53f..8dafae86 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -41,7 +41,6 @@ import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; import com.android.internal.annotations.VisibleForTesting; @@ -161,9 +160,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio finishCallback, logger), chooserRequest.getChooserActions(), - (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) - ? chooserRequest.getModifyShareAction() - : null), + chooserRequest.getModifyShareAction(), onUpdateSharedTextIsExcluded, logger, finishCallback); diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index d11561df..f9004a9b 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -33,7 +33,6 @@ import android.util.Log; import android.util.Pair; import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.google.common.collect.ImmutableList; @@ -147,9 +146,7 @@ public class ChooserRequestParameters { mTargetIntentFilter = getTargetIntentFilter(mTarget); mChooserActions = getChooserActions(clientIntent); - mModifyShareAction = featureFlags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION) - ? getModifyShareAction(clientIntent) - : null; + mModifyShareAction = getModifyShareAction(clientIntent); } public Intent getTargetIntent() { diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 05d0ee66..15ba96c0 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -37,8 +37,6 @@ import android.widget.TextView; import androidx.annotation.LayoutRes; import com.android.intentresolver.R; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; import java.util.ArrayList; @@ -129,11 +127,9 @@ abstract class ContentPreviewUi { protected static void displayModifyShareAction( ViewGroup layout, - ChooserContentPreviewUi.ActionFactory actionFactory, - FeatureFlagRepository featureFlagRepository) { + ChooserContentPreviewUi.ActionFactory actionFactory) { ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction(); - if (modifyShareAction != null && layout != null - && featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) { + if (modifyShareAction != null && layout != null) { TextView modifyShareView = layout.findViewById(R.id.reselection_action); if (modifyShareView != null) { modifyShareView.setText(modifyShareAction.getLabel()); diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index cc087a63..e65538ae 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -68,7 +68,7 @@ class FileContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(resources, layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory); return layout; } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index 1ca2ba61..cf7c5525 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -83,7 +83,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory); return layout; } diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index 8a3c2259..6b5676cc 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -73,7 +73,7 @@ class TextContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory); return layout; } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index dfc171d2..55e8058b 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -89,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { @Override public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory, mFeatureFlagRepository); + displayModifyShareAction(layout, mActionFactory); return layout; } diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 846f8e64..2b3da725 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -22,14 +22,9 @@ import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to // make the flags available in the flag flipper app (see go/sysui-flags). object Flags { - const val SHARESHEET_RESELECTION_ACTION_NAME = "sharesheet_reselection_action" const val SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME = "sharesheet_image_text_preview" const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview" - // TODO(b/266982749) Tracking Bug - @JvmField - val SHARESHEET_RESELECTION_ACTION = releasedFlag(1502, SHARESHEET_RESELECTION_ACTION_NAME) - // TODO(b/266983474) Tracking Bug @JvmField val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = releasedFlag( diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt index 98c7d5ee..0a8c22b7 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -71,7 +71,6 @@ class ChooserActionFactoryTest { @Before fun setup() { - whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(true) context.registerReceiver(testReceiver, IntentFilter(testAction)) } @@ -105,14 +104,6 @@ class ChooserActionFactoryTest { assertThat(factory.modifyShareAction).isNull() } - @Test - fun testNoModifyShareAction_flagDisabled() { - whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(false) - val factory = createFactory(includeModifyShare = true) - - assertThat(factory.modifyShareAction).isNull() - } - @Test fun testModifyShareAction() { val factory = createFactory(includeModifyShare = true) diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 088e8923..b6a3631b 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -169,7 +169,6 @@ public class UnbundledChooserActivityTest { private static final List ALL_FLAGS = Arrays.asList( - Flags.SHARESHEET_RESELECTION_ACTION, Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); @@ -1761,9 +1760,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_RESELECTION_ACTION_NAME }, - values = { true }) public void testLaunchWithShareModification() throws InterruptedException { List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); -- cgit v1.2.3-59-g8ed1b From c75013c73c79fac3c9f64661d0e09825c5bde345 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 4 Apr 2023 20:14:55 +0000 Subject: Remove usage of SHARESHEET_IMAGE_AND_TEXT_PREVIEW Bug: 266983474 Test: atest IntentResolverUnitTests Change-Id: I5571fde968fb1bc0280ccdfe43f26d2843bec7ae --- .../contentpreview/ImageContentPreviewUi.java | 6 +----- .../contentpreview/UnifiedContentPreviewUi.java | 6 +----- java/src/com/android/intentresolver/flags/Flags.kt | 7 ------- .../intentresolver/UnbundledChooserActivityTest.java | 16 ---------------- 4 files changed, 2 insertions(+), 33 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java index cf7c5525..85ae2adb 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java @@ -37,7 +37,6 @@ import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ChooserImagePreviewView; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; @@ -157,10 +156,7 @@ class ImageContentPreviewUi extends ContentPreviewUi { private void setTextInImagePreviewVisibility( ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { - int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) - && !TextUtils.isEmpty(mText) - ? View.VISIBLE - : View.GONE; + int visibility = !TextUtils.isEmpty(mText) ? View.VISIBLE : View.GONE; final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 55e8058b..a849ddb9 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -36,7 +36,6 @@ import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import com.android.intentresolver.widget.ScrollableImagePreviewView; @@ -139,10 +138,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mFiles.size() - previews.size(), mImageLoader); - if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW) - && !TextUtils.isEmpty(mText) - && mFiles.size() == 1 - && allImages) { + if (!TextUtils.isEmpty(mText) && mFiles.size() == 1 && allImages) { setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); updateTextWithImageHeadline(contentPreviewLayout); } else { diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 2b3da725..0440b3d6 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -22,15 +22,8 @@ import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to // make the flags available in the flag flipper app (see go/sysui-flags). object Flags { - const val SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME = "sharesheet_image_text_preview" const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview" - // TODO(b/266983474) Tracking Bug - @JvmField - val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = releasedFlag( - id = 1503, name = SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME - ) - // TODO(b/267355521) Tracking Bug @JvmField val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag( diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index b6a3631b..d074e978 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -169,7 +169,6 @@ public class UnbundledChooserActivityTest { private static final List ALL_FLAGS = Arrays.asList( - Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW, Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); private static final Map ALL_FLAGS_OFF = @@ -682,9 +681,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testImagePlusTextSharing_ExcludeText() { Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" @@ -725,9 +721,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testImagePlusTextSharing_RemoveAndAddBackText() { Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" @@ -772,9 +765,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { Intent sendIntent = createSendImageIntent( Uri.parse("android.resource://com.android.frameworks.coretests/" @@ -1048,9 +1038,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testImageAndTextPreview() { final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); @@ -1074,9 +1061,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME }, - values = { true }) public void testNoTextPreviewWhenTextIsSharedWithMultipleImages() { final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); -- cgit v1.2.3-59-g8ed1b From 964676869a6edcacccae658ba6ab4290f90166c2 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 4 Apr 2023 21:00:37 +0000 Subject: Remove SHARESHEET_SCROLLABLE_IMAGE_PREVIEW Also simplify some flag testing stuff when there are no flags being tested. Bug: 267355521 Test: atest IntentResolverUnitTests Change-Id: I2ec39d38605d4e1b9df5bb22542623cdeb1e51da --- java/res/layout/chooser_image_preview_view.xml | 26 --- .../chooser_image_preview_view_internals.xml | 73 -------- .../android/intentresolver/ChooserActivity.java | 16 +- .../contentpreview/ChooserContentPreviewUi.java | 51 +----- .../contentpreview/FileContentPreviewUi.java | 4 - .../contentpreview/ImageContentPreviewUi.java | 188 --------------------- .../contentpreview/TextContentPreviewUi.java | 4 - .../contentpreview/UnifiedContentPreviewUi.java | 4 - java/src/com/android/intentresolver/flags/Flags.kt | 9 +- .../widget/ChooserImagePreviewView.kt | 163 ------------------ .../UnbundledChooserActivityTest.java | 81 ++------- .../contentpreview/ChooserContentPreviewUiTest.kt | 18 -- 12 files changed, 19 insertions(+), 618 deletions(-) delete mode 100644 java/res/layout/chooser_image_preview_view.xml delete mode 100644 java/res/layout/chooser_image_preview_view_internals.xml delete mode 100644 java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java delete mode 100644 java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt (limited to 'java/src') diff --git a/java/res/layout/chooser_image_preview_view.xml b/java/res/layout/chooser_image_preview_view.xml deleted file mode 100644 index e81349c7..00000000 --- a/java/res/layout/chooser_image_preview_view.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - diff --git a/java/res/layout/chooser_image_preview_view_internals.xml b/java/res/layout/chooser_image_preview_view_internals.xml deleted file mode 100644 index 2b93edf8..00000000 --- a/java/res/layout/chooser_image_preview_view_internals.xml +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 270fc299..dd0be4f0 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -87,7 +87,6 @@ import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.grid.DirectShareViewHolder; import com.android.intentresolver.model.AbstractResolverComparator; @@ -293,7 +292,6 @@ public class ChooserActivity extends ResolverActivity implements createPreviewImageLoader(), createChooserActionFactory(), mEnterTransitionAnimationDelegate, - mFeatureFlagRepository, new HeadlineGeneratorImpl(this)); setAdditionalTargets(mChooserRequest.getAdditionalTargets()); @@ -1341,15 +1339,11 @@ public class ChooserActivity extends ResolverActivity implements @VisibleForTesting protected ImageLoader createPreviewImageLoader() { final int cacheSize; - if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { - float chooserWidth = getResources().getDimension(R.dimen.chooser_width); - // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView) - float imageWidth = - getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2; - cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); - } else { - cacheSize = 3; - } + float chooserWidth = getResources().getDimension(R.dimen.chooser_width); + // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView) + float imageWidth = + getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2; + cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize); } diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 3509c67d..318aa627 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -39,14 +39,11 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.function.Consumer; /** @@ -103,7 +100,6 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { mContentPreviewUi = createContentPreview( @@ -113,7 +109,6 @@ public final class ChooserContentPreviewUi { imageLoader, actionFactory, transitionElementStatusCallback, - featureFlagRepository, headlineGenerator); if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) { transitionElementStatusCallback.onAllTransitionElementsReady(); @@ -127,7 +122,6 @@ public final class ChooserContentPreviewUi { ImageLoader imageLoader, ActionFactory actionFactory, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { /* In {@link android.content.Intent#getType}, the app may specify a very general mime type @@ -145,7 +139,6 @@ public final class ChooserContentPreviewUi { targetIntent, actionFactory, imageLoader, - featureFlagRepository, headlineGenerator); } List uris = extractContentUris(targetIntent); @@ -154,7 +147,6 @@ public final class ChooserContentPreviewUi { targetIntent, actionFactory, imageLoader, - featureFlagRepository, headlineGenerator); } ArrayList files = new ArrayList<>(uris.size()); @@ -164,50 +156,15 @@ public final class ChooserContentPreviewUi { files, actionFactory, imageLoader, - featureFlagRepository, headlineGenerator); } - if (featureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) { - return new UnifiedContentPreviewUi( - files, - targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), - actionFactory, - imageLoader, - typeClassifier, - transitionElementStatusCallback, - featureFlagRepository, - headlineGenerator); - } - if (previewCount < uris.size()) { - return new FileContentPreviewUi( - files, - actionFactory, - imageLoader, - featureFlagRepository, - headlineGenerator); - } - // The legacy (3-image) image preview is on it's way out and it's unlikely that we'd end up - // here. To preserve the legacy behavior, before using it, check that all uris are images. - for (FileInfo fileInfo: files) { - if (!typeClassifier.isImageType(fileInfo.getMimeType())) { - return new FileContentPreviewUi( - files, - actionFactory, - imageLoader, - featureFlagRepository, - headlineGenerator); - } - } - return new ImageContentPreviewUi( - files.stream() - .map(FileInfo::getPreviewUri) - .filter(Objects::nonNull) - .toList(), + return new UnifiedContentPreviewUi( + files, targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), actionFactory, imageLoader, + typeClassifier, transitionElementStatusCallback, - featureFlagRepository, headlineGenerator); } @@ -323,7 +280,6 @@ public final class ChooserContentPreviewUi { Intent targetIntent, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE); @@ -341,7 +297,6 @@ public final class ChooserContentPreviewUi { previewThumbnail, actionFactory, imageLoader, - featureFlagRepository, headlineGenerator); } diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index e65538ae..3012eec2 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -29,7 +29,6 @@ import androidx.annotation.LayoutRes; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; -import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.widget.ActionRow; import java.util.ArrayList; @@ -44,19 +43,16 @@ class FileContentPreviewUi extends ContentPreviewUi { private final List mFiles; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; - private final FeatureFlagRepository mFeatureFlagRepository; private final HeadlineGenerator mHeadlineGenerator; FileContentPreviewUi( List files, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { mFiles = files; mActionFactory = actionFactory; mImageLoader = imageLoader; - mFeatureFlagRepository = featureFlagRepository; mHeadlineGenerator = headlineGenerator; } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java deleted file mode 100644 index 85ae2adb..00000000 --- a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java +++ /dev/null @@ -1,188 +0,0 @@ -/* - * 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.contentpreview; - -import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; - -import android.content.res.Resources; -import android.net.Uri; -import android.text.TextUtils; -import android.text.util.Linkify; -import android.transition.TransitionManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewStub; -import android.widget.CheckBox; -import android.widget.TextView; - -import androidx.annotation.LayoutRes; -import androidx.annotation.Nullable; - -import com.android.intentresolver.ImageLoader; -import com.android.intentresolver.R; -import com.android.intentresolver.flags.FeatureFlagRepository; -import com.android.intentresolver.widget.ActionRow; -import com.android.intentresolver.widget.ChooserImagePreviewView; -import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; - -class ImageContentPreviewUi extends ContentPreviewUi { - private final List mImageUris; - @Nullable - private final CharSequence mText; - private final ChooserContentPreviewUi.ActionFactory mActionFactory; - private final ImageLoader mImageLoader; - private final TransitionElementStatusCallback mTransitionElementStatusCallback; - private final FeatureFlagRepository mFeatureFlagRepository; - private final HeadlineGenerator mHeadlineGenerator; - - ImageContentPreviewUi( - List imageUris, - @Nullable CharSequence text, - ChooserContentPreviewUi.ActionFactory actionFactory, - ImageLoader imageLoader, - TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, - HeadlineGenerator headlineGenerator) { - mImageUris = imageUris; - mText = text; - mActionFactory = actionFactory; - mImageLoader = imageLoader; - mTransitionElementStatusCallback = transitionElementStatusCallback; - mFeatureFlagRepository = featureFlagRepository; - mHeadlineGenerator = headlineGenerator; - - mImageLoader.prePopulate(mImageUris); - } - - @Override - public int getType() { - return CONTENT_PREVIEW_IMAGE; - } - - @Override - public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { - ViewGroup layout = displayInternal(layoutInflater, parent); - displayModifyShareAction(layout, mActionFactory); - return layout; - } - - private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(); - ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( - R.layout.chooser_grid_preview_image, parent, false); - ChooserImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); - - final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); - if (actionRow != null) { - actionRow.setActions( - createActions( - createImagePreviewActions(), - mActionFactory.createCustomActions())); - } - - if (mImageUris.size() == 0) { - Log.i( - TAG, - "Attempted to display image preview area with zero" - + " available images detected in EXTRA_STREAM list"); - imagePreview.setVisibility(View.GONE); - mTransitionElementStatusCallback.onAllTransitionElementsReady(); - return contentPreviewLayout; - } - - setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); - imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback); - imagePreview.setImages(mImageUris, mImageLoader); - - updateHeadline(contentPreviewLayout); - - return contentPreviewLayout; - } - - private List createImagePreviewActions() { - ArrayList actions = new ArrayList<>(2); - //TODO: add copy action; - ActionRow.Action action = mActionFactory.createNearbyButton(); - if (action != null) { - actions.add(action); - } - action = mActionFactory.createEditButton(); - if (action != null) { - actions.add(action); - } - return actions; - } - - private ChooserImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) { - ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub); - if (stub != null) { - stub.setLayoutResource(R.layout.chooser_image_preview_view); - stub.inflate(); - } - return previewLayout.findViewById( - com.android.internal.R.id.content_preview_image_area); - } - - private void updateHeadline(ViewGroup contentPreview) { - CheckBox includeTextCheckbox = contentPreview.requireViewById(R.id.include_text_action); - if (includeTextCheckbox.getVisibility() == View.VISIBLE - && includeTextCheckbox.isChecked()) { - displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); - } else { - displayHeadline( - contentPreview, mHeadlineGenerator.getImagesHeadline(mImageUris.size())); - } - } - - private void setTextInImagePreviewVisibility( - ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { - int visibility = !TextUtils.isEmpty(mText) ? View.VISIBLE : View.GONE; - - final TextView textView = contentPreview - .requireViewById(com.android.internal.R.id.content_preview_text); - CheckBox actionView = contentPreview - .requireViewById(R.id.include_text_action); - textView.setVisibility(visibility); - boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString()); - textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); - textView.setText(mText); - - if (visibility == View.VISIBLE) { - final int[] actionLabels = isLink - ? new int[] { R.string.include_link, R.string.exclude_link } - : new int[] { R.string.include_text, R.string.exclude_text }; - final Consumer shareTextAction = actionFactory.getExcludeSharedTextAction(); - actionView.setChecked(true); - actionView.setText(actionLabels[1]); - shareTextAction.accept(false); - actionView.setOnCheckedChangeListener((view, isChecked) -> { - view.setText(actionLabels[isChecked ? 1 : 0]); - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - shareTextAction.accept(!isChecked); - updateHeadline(contentPreview); - }); - } - actionView.setVisibility(visibility); - } -} diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index 6b5676cc..70df6479 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -30,7 +30,6 @@ import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; -import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.widget.ActionRow; import java.util.ArrayList; @@ -45,7 +44,6 @@ class TextContentPreviewUi extends ContentPreviewUi { private final Uri mPreviewThumbnail; private final ImageLoader mImageLoader; private final ChooserContentPreviewUi.ActionFactory mActionFactory; - private final FeatureFlagRepository mFeatureFlagRepository; private final HeadlineGenerator mHeadlineGenerator; TextContentPreviewUi( @@ -54,14 +52,12 @@ class TextContentPreviewUi extends ContentPreviewUi { @Nullable Uri previewThumbnail, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { mSharingText = sharingText; mPreviewTitle = previewTitle; mPreviewThumbnail = previewThumbnail; mImageLoader = imageLoader; mActionFactory = actionFactory; - mFeatureFlagRepository = featureFlagRepository; mHeadlineGenerator = headlineGenerator; } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index a849ddb9..00a11e30 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -35,7 +35,6 @@ import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; -import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; import com.android.intentresolver.widget.ScrollableImagePreviewView; @@ -53,7 +52,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { private final ImageLoader mImageLoader; private final MimeTypeClassifier mTypeClassifier; private final TransitionElementStatusCallback mTransitionElementStatusCallback; - private final FeatureFlagRepository mFeatureFlagRepository; private final HeadlineGenerator mHeadlineGenerator; UnifiedContentPreviewUi( @@ -63,7 +61,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, - FeatureFlagRepository featureFlagRepository, HeadlineGenerator headlineGenerator) { mFiles = files; mText = text; @@ -71,7 +68,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mImageLoader = imageLoader; mTypeClassifier = typeClassifier; mTransitionElementStatusCallback = transitionElementStatusCallback; - mFeatureFlagRepository = featureFlagRepository; mHeadlineGenerator = headlineGenerator; mImageLoader.prePopulate(mFiles.stream() diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt index 0440b3d6..b303dd1a 100644 --- a/java/src/com/android/intentresolver/flags/Flags.kt +++ b/java/src/com/android/intentresolver/flags/Flags.kt @@ -21,15 +21,8 @@ import com.android.systemui.flags.UnreleasedFlag // Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to // make the flags available in the flag flipper app (see go/sysui-flags). +// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS. object Flags { - const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview" - - // TODO(b/267355521) Tracking Bug - @JvmField - val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = releasedFlag( - 1504, SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME - ) - private fun releasedFlag(id: Int, name: String) = ReleasedFlag(id, name, "systemui") diff --git a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt deleted file mode 100644 index 6273296d..00000000 --- a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright (C) 2022 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.widget - -import android.animation.ObjectAnimator -import android.content.Context -import android.net.Uri -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.animation.DecelerateInterpolator -import android.widget.RelativeLayout -import androidx.core.view.isVisible -import com.android.intentresolver.R -import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback -import kotlinx.coroutines.Job -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import com.android.internal.R as IntR - -private const val IMAGE_FADE_IN_MILLIS = 150L - -class ChooserImagePreviewView : RelativeLayout, ImagePreviewView { - - constructor(context: Context) : this(context, null) - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - - constructor( - context: Context, attrs: AttributeSet?, defStyleAttr: Int - ) : this(context, attrs, defStyleAttr, 0) - - constructor( - context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int - ) : super(context, attrs, defStyleAttr, defStyleRes) - - private val coroutineScope = MainScope() - private lateinit var mainImage: RoundedRectImageView - private lateinit var secondLargeImage: RoundedRectImageView - private lateinit var secondSmallImage: RoundedRectImageView - private lateinit var thirdImage: RoundedRectImageView - - private var loadImageJob: Job? = null - private var transitionStatusElementCallback: TransitionElementStatusCallback? = null - - override fun onFinishInflate() { - LayoutInflater.from(context) - .inflate(R.layout.chooser_image_preview_view_internals, this, true) - mainImage = requireViewById(IntR.id.content_preview_image_1_large) - secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large) - secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small) - thirdImage = requireViewById(IntR.id.content_preview_image_3_small) - } - - /** - * Specifies a transition animation target readiness callback. The callback will be - * invoked once when views preparation is done. - * Should be called before [setImages]. - */ - override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) { - transitionStatusElementCallback = callback - } - - fun setImages(uris: List, imageLoader: ImageLoader) { - loadImageJob?.cancel() - loadImageJob = coroutineScope.launch { - when (uris.size) { - 0 -> hideAllViews() - 1 -> showOneImage(uris, imageLoader) - 2 -> showTwoImages(uris, imageLoader) - else -> showThreeImages(uris, imageLoader) - } - } - } - - private fun hideAllViews() { - mainImage.isVisible = false - secondLargeImage.isVisible = false - secondSmallImage.isVisible = false - thirdImage.isVisible = false - invokeTransitionViewReadyCallback() - } - - private suspend fun showOneImage(uris: List, imageLoader: ImageLoader) { - secondLargeImage.isVisible = false - secondSmallImage.isVisible = false - thirdImage.isVisible = false - showImages(uris, imageLoader, mainImage) - } - - private suspend fun showTwoImages(uris: List, imageLoader: ImageLoader) { - secondSmallImage.isVisible = false - thirdImage.isVisible = false - showImages(uris, imageLoader, mainImage, secondLargeImage) - } - - private suspend fun showThreeImages(uris: List, imageLoader: ImageLoader) { - secondLargeImage.isVisible = false - showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage) - thirdImage.setExtraImageCount(uris.size - 3) - } - - private suspend fun showImages( - uris: List, imageLoader: ImageLoader, vararg views: RoundedRectImageView - ) = coroutineScope { - for (i in views.indices) { - launch { - loadImageIntoView(views[i], uris[i], imageLoader) - } - } - } - - private suspend fun loadImageIntoView( - view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader - ) { - val bitmap = runCatching { - imageLoader(uri) - }.getOrDefault(null) - if (bitmap == null) { - view.isVisible = false - if (view === mainImage) { - invokeTransitionViewReadyCallback() - } - } else { - view.isVisible = true - view.setImageBitmap(bitmap) - - view.alpha = 0f - ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply { - interpolator = DecelerateInterpolator(1.0f) - duration = IMAGE_FADE_IN_MILLIS - start() - } - if (view === mainImage && transitionStatusElementCallback != null) { - view.waitForPreDraw() - invokeTransitionViewReadyCallback() - } - } - } - - private fun invokeTransitionViewReadyCallback() { - transitionStatusElementCallback?.apply { - if (mainImage.isVisible && mainImage.drawable != null) { - mainImage.transitionName?.let { onTransitionElementReady(it) } - } - onAllTransitionElementsReady() - } - transitionStatusElementCallback = null - } -} diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index d074e978..eb340224 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -104,7 +104,6 @@ import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.chooser.DisplayResolveInfo; -import com.android.intentresolver.flags.Flags; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -168,8 +167,7 @@ public class UnbundledChooserActivityTest { }; private static final List ALL_FLAGS = - Arrays.asList( - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW); + Arrays.asList(); private static final Map ALL_FLAGS_OFF = createAllFlagsOverride(false); @@ -178,6 +176,15 @@ public class UnbundledChooserActivityTest { @Parameterized.Parameters public static Collection packageManagers() { + if (ALL_FLAGS.isEmpty()) { + // No flags to toggle between, so just two configurations. + return Arrays.asList(new Object[][] { + // Default PackageManager and all flags off + { DEFAULT_PM, ALL_FLAGS_OFF}, + // No App Prediction Service and all flags off + { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF }, + }); + } return Arrays.asList(new Object[][] { // Default PackageManager and all flags off { DEFAULT_PM, ALL_FLAGS_OFF}, @@ -933,74 +940,6 @@ public class UnbundledChooserActivityTest { } @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { false }) - public void twoVisibleImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); - - ArrayList uris = new ArrayList<>(); - uris.add(uri); - uris.add(uri); - - Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; - - List resolvedComponentInfos = createResolvedComponentsForTest(2); - - setupResolverControllers(resolvedComponentInfos); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_1_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_small)) - .check(matches(not(isDisplayed()))); - onView(withId(com.android.internal.R.id.content_preview_image_3_small)) - .check(matches(not(isDisplayed()))); - } - - @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { false }) - public void threeOrMoreVisibleImagePreview() { - Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240); - - ArrayList uris = new ArrayList<>(); - uris.add(uri); - uris.add(uri); - uris.add(uri); - uris.add(uri); - uris.add(uri); - - Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); - ChooserActivityOverrideData.getInstance().isImageType = true; - - List resolvedComponentInfos = createResolvedComponentsForTest(2); - - setupResolverControllers(resolvedComponentInfos); - mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); - waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_1_large)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_2_large)) - .check(matches(not(isDisplayed()))); - onView(withId(com.android.internal.R.id.content_preview_image_2_small)) - .check(matches(isDisplayed())); - onView(withId(com.android.internal.R.id.content_preview_image_3_small)) - .check(matches(isDisplayed())); - } - - @Test - @RequireFeatureFlags( - flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME }, - values = { true }) public void testManyVisibleImagePreview_ScrollableImagePreview() { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 82bf94c4..7b9a0ce6 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -22,11 +22,9 @@ import android.content.Intent import android.graphics.Bitmap import android.net.Uri import com.android.intentresolver.ImageLoader -import com.android.intentresolver.TestFeatureFlagRepository import com.android.intentresolver.any import com.android.intentresolver.anyOrNull import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory -import com.android.intentresolver.flags.Flags import com.android.intentresolver.mock import com.android.intentresolver.whenever import com.android.intentresolver.widget.ActionRow @@ -61,11 +59,6 @@ class ChooserContentPreviewUiTest { override fun getExcludeSharedTextAction(): Consumer = Consumer {} } private val transitionCallback = mock() - private val featureFlagRepository = TestFeatureFlagRepository( - mapOf( - Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW to true - ) - ) @Test fun test_ChooserContentPreview_non_send_intent_action_to_text_preview() { @@ -77,7 +70,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -98,7 +90,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -120,7 +111,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -142,7 +132,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -165,7 +154,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -191,7 +179,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -215,7 +202,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -245,7 +231,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -275,7 +260,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -307,7 +291,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) @@ -337,7 +320,6 @@ class ChooserContentPreviewUiTest { imageLoader, actionFactory, transitionCallback, - featureFlagRepository, headlineGenerator ) assertThat(testSubject.preferredContentPreview) -- cgit v1.2.3-59-g8ed1b From 35ea80ec66daa4df6e77766571c29a651c4db8f0 Mon Sep 17 00:00:00 2001 From: Liana Kazanova Date: Thu, 6 Apr 2023 23:01:18 +0000 Subject: Revert "Adds a reciever for pin migration data" Revert submission 22119004-b223249318-chooser-pin-migrate Reason for revert: Reverted changes: /q/submissionid:22119004-b223249318-chooser-pin-migrate Change-Id: I17b20318a401e9942f941b1be2ce5b842237b487 --- AndroidManifest.xml | 10 +-- .../intentresolver/ChooserPinMigrationReceiver.kt | 55 --------------- .../ChooserPinMigrationReceiverTest.kt | 78 ---------------------- 3 files changed, 1 insertion(+), 142 deletions(-) delete mode 100644 java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt delete mode 100644 java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt (limited to 'java/src') diff --git a/AndroidManifest.xml b/AndroidManifest.xml index a3acc8a6..fc99c0b2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -22,6 +22,7 @@ android:versionName="2021-11" coreApp="true"> + @@ -31,7 +32,6 @@ - @@ -71,14 +71,6 @@ - - - - - - diff --git a/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt b/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt deleted file mode 100644 index a3ba2192..00000000 --- a/java/src/com/android/intentresolver/ChooserPinMigrationReceiver.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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 - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.util.Log - -/** - * Broadcast receiver for receiving Chooser pin data from the legacy chooser. - * - * Unions the legacy pins with any existing pins. This receiver is protected by the ADD_CHOOSER_PINS - * permission. The receiver is required to have the RECEIVE_CHOOSER_PIN_MIGRATION to receive the - * broadcast. - */ -class ChooserPinMigrationReceiver( - private val pinnedSharedPrefsProvider: (Context) -> SharedPreferences = - { context -> ChooserActivity.getPinnedSharedPrefs(context) }, -) : BroadcastReceiver() { - - override fun onReceive(context: Context, intent: Intent) { - val bundle = intent.extras ?: return - Log.i(TAG, "Starting migration") - - val prefsEditor = pinnedSharedPrefsProvider.invoke(context).edit() - bundle.keySet().forEach { key -> - if(bundle.getBoolean(key)) { - prefsEditor.putBoolean(key, true) - } - } - prefsEditor.apply() - - Log.i(TAG, "Migration complete") - } - - companion object { - private const val TAG = "ChooserPinMigrationReceiver" - } -} \ No newline at end of file diff --git a/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt b/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt deleted file mode 100644 index 1daee137..00000000 --- a/java/tests/src/com/android/intentresolver/ChooserPinMigrationReceiverTest.kt +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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 - -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import androidx.test.ext.junit.runners.AndroidJUnit4 -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyNoMoreInteractions -import org.mockito.MockitoAnnotations - -@RunWith(AndroidJUnit4::class) -class ChooserPinMigrationReceiverTest { - - private lateinit var chooserPinMigrationReceiver: ChooserPinMigrationReceiver - - @Mock private lateinit var mockContext: Context - @Mock private lateinit var mockSharedPreferences: SharedPreferences - @Mock private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - - whenever(mockSharedPreferences.edit()).thenReturn(mockSharedPreferencesEditor) - - chooserPinMigrationReceiver = ChooserPinMigrationReceiver { mockSharedPreferences } - } - - @Test - fun onReceive_addsReceivedPins() { - // Arrange - val intent = Intent().apply { - putExtra("TestPackage/TestClass", true) - } - - // Act - chooserPinMigrationReceiver.onReceive(mockContext, intent) - - // Assert - verify(mockSharedPreferencesEditor).putBoolean(eq("TestPackage/TestClass"), eq(true)) - verify(mockSharedPreferencesEditor).apply() - } - - @Test - fun onReceive_ignoresUnpinnedEntries() { - // Arrange - val intent = Intent().apply { - putExtra("TestPackage/TestClass", false) - } - - // Act - chooserPinMigrationReceiver.onReceive(mockContext, intent) - - // Assert - verify(mockSharedPreferencesEditor).apply() - verifyNoMoreInteractions(mockSharedPreferencesEditor) - } -} \ No newline at end of file -- cgit v1.2.3-59-g8ed1b From a36c4deb98e45aa0994b3f5fe2937fb112d03451 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Wed, 5 Apr 2023 21:34:47 -0700 Subject: Replace view stubs with actual views With the feature flags removed, there are no layout variations for action row and image preivew anymore thus ViewStubs can be replaced with the actual views. Bug: 267355521 Bug: 266983432 Test: manual testing Change-Id: I96a3a3560bfa5f1fbf064bd6080c618467c7aca1 --- java/res/layout/chooser_grid_preview_file.xml | 10 ++++--- java/res/layout/chooser_grid_preview_image.xml | 24 ++++++++++----- java/res/layout/chooser_grid_preview_text.xml | 11 ++++--- java/res/layout/scrollable_chooser_action_row.xml | 30 ------------------- java/res/layout/scrollable_image_preview_view.xml | 35 ---------------------- .../contentpreview/ContentPreviewUi.java | 16 ---------- .../contentpreview/FileContentPreviewUi.java | 16 ++++------ .../contentpreview/TextContentPreviewUi.java | 15 ++++------ .../contentpreview/UnifiedContentPreviewUi.java | 30 ++++++------------- .../UnbundledChooserActivityTest.java | 17 ++++------- 10 files changed, 55 insertions(+), 149 deletions(-) delete mode 100644 java/res/layout/scrollable_chooser_action_row.xml delete mode 100644 java/res/layout/scrollable_image_preview_view.xml (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index 036c5318..bcc320d3 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -71,10 +71,12 @@ android:textAppearance="@style/TextAppearance.ChooserDefault" /> - + diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 9ad594e8..43f6f4d1 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -19,6 +19,7 @@ - + android:layout_height="@dimen/chooser_preview_image_height_tall" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="@dimen/chooser_view_spacing" + android:background="?android:attr/colorBackground" + app:itemInnerSpacing="3dp" + app:itemOuterSpacing="@dimen/chooser_edge_margin_normal" + app:maxWidthHint="@dimen/chooser_width" /> - + diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml index 47beaa5a..5e7afa46 100644 --- a/java/res/layout/chooser_grid_preview_text.xml +++ b/java/res/layout/chooser_grid_preview_text.xml @@ -84,9 +84,12 @@ android:focusable="true"/> - + + diff --git a/java/res/layout/scrollable_chooser_action_row.xml b/java/res/layout/scrollable_chooser_action_row.xml deleted file mode 100644 index cb5dabf0..00000000 --- a/java/res/layout/scrollable_chooser_action_row.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - diff --git a/java/res/layout/scrollable_image_preview_view.xml b/java/res/layout/scrollable_image_preview_view.xml deleted file mode 100644 index 0d41f1ae..00000000 --- a/java/res/layout/scrollable_image_preview_view.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java index 15ba96c0..fcafe752 100644 --- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java @@ -29,13 +29,10 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewStub; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.LayoutRes; - import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -52,19 +49,6 @@ abstract class ContentPreviewUi { public abstract ViewGroup display( Resources resources, LayoutInflater layoutInflater, ViewGroup parent); - protected static int getActionRowLayout() { - return R.layout.scrollable_chooser_action_row; - } - - protected static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) { - final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub); - if (stub != null) { - stub.setLayoutResource(actionRowLayout); - stub.inflate(); - } - return parent.findViewById(com.android.internal.R.id.chooser_action_row); - } - protected static List createActions( List systemActions, List customActions) { diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index 3012eec2..e814eb12 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -25,8 +25,6 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.LayoutRes; - import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -70,7 +68,6 @@ class FileContentPreviewUi extends ContentPreviewUi { private ViewGroup displayInternal( Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_file, parent, false); @@ -110,13 +107,12 @@ class FileContentPreviewUi extends ContentPreviewUi { fileIconView.setImageResource(R.drawable.ic_file_copy); } - final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); - if (actionRow != null) { - actionRow.setActions( - createActions( - createFilePreviewActions(), - mActionFactory.createCustomActions())); - } + final ActionRow actionRow = + contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + actionRow.setActions( + createActions( + createFilePreviewActions(), + mActionFactory.createCustomActions())); return contentPreviewLayout; } diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index 70df6479..ece0c312 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -25,7 +25,6 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; -import androidx.annotation.LayoutRes; import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; @@ -76,17 +75,15 @@ class TextContentPreviewUi extends ContentPreviewUi { private ViewGroup displayInternal( LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_text, parent, false); - final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); - if (actionRow != null) { - actionRow.setActions( - createActions( - createTextPreviewActions(), - mActionFactory.createCustomActions())); - } + final ActionRow actionRow = + contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + actionRow.setActions( + createActions( + createTextPreviewActions(), + mActionFactory.createCustomActions())); if (mSharingText == null) { contentPreviewLayout diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 00a11e30..748f7421 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -26,11 +26,9 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewStub; import android.widget.CheckBox; import android.widget.TextView; -import androidx.annotation.LayoutRes; import androidx.annotation.Nullable; import com.android.intentresolver.ImageLoader; @@ -89,18 +87,17 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { } private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { - @LayoutRes int actionRowLayout = getActionRowLayout(); ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( R.layout.chooser_grid_preview_image, parent, false); - ScrollableImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout); - - final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout); - if (actionRow != null) { - actionRow.setActions( - createActions( - createImagePreviewActions(), - mActionFactory.createCustomActions())); - } + ScrollableImagePreviewView imagePreview = + contentPreviewLayout.findViewById(R.id.scrollable_image_preview); + + final ActionRow actionRow = + contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + actionRow.setActions( + createActions( + createImagePreviewActions(), + mActionFactory.createCustomActions())); if (mFiles.size() == 0) { Log.i( @@ -167,15 +164,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return actions; } - private ScrollableImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) { - ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub); - if (stub != null) { - stub.setLayoutResource(R.layout.scrollable_image_preview_view); - stub.inflate(); - } - return previewLayout.findViewById(R.id.scrollable_image_preview); - } - private void updateTextWithImageHeadline(ViewGroup contentPreview) { CheckBox actionView = contentPreview.requireViewById(R.id.include_text_action); if (actionView.getVisibility() == View.VISIBLE && actionView.isChecked()) { diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index eb340224..39357a4d 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -91,7 +91,6 @@ import android.util.HashedStringCache; import android.util.Pair; import android.util.SparseArray; import android.view.View; -import android.view.ViewGroup; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; @@ -919,23 +918,17 @@ public class UnbundledChooserActivityTest { setupResolverControllers(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_image_area)) + onView(withId(R.id.scrollable_image_preview)) .check((view, exception) -> { if (exception != null) { throw exception; } - ViewGroup parent = (ViewGroup) view; - ArrayList visibleViews = new ArrayList<>(); - for (int i = 0, count = parent.getChildCount(); i < count; i++) { - View child = parent.getChildAt(i); - if (child.getVisibility() == View.VISIBLE) { - visibleViews.add(child); - } - } - assertThat(visibleViews.size(), is(1)); + RecyclerView recyclerView = (RecyclerView) view; + assertThat(recyclerView.getAdapter().getItemCount(), is(1)); + assertThat(recyclerView.getChildCount(), is(1)); assertThat( "image preview view is fully visible", - isDisplayed().matches(visibleViews.get(0))); + isDisplayed().matches(recyclerView.getChildAt(0))); }); } -- cgit v1.2.3-59-g8ed1b From 9721cfb08324587d9649c6630750e599b67352db Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Thu, 6 Apr 2023 19:37:59 -0700 Subject: Remove content preview from FileContentPreviewUI UnifiedContentPreviewUi is now used if any URI has a preview and the legacy FileContentPreviewUi's content preview logic can be removed. Bug: 271613784 Test: manual testing Change-Id: I24278fc0c2f88dc8517bd625bd8645a38f47c945 --- java/res/layout/chooser_grid_preview_file.xml | 12 +---- .../contentpreview/ChooserContentPreviewUi.java | 1 - .../contentpreview/FileContentPreviewUi.java | 62 +++++----------------- 3 files changed, 15 insertions(+), 60 deletions(-) (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml index bcc320d3..7e308e68 100644 --- a/java/res/layout/chooser_grid_preview_file.xml +++ b/java/res/layout/chooser_grid_preview_file.xml @@ -39,15 +39,6 @@ android:layout_marginBottom="@dimen/chooser_view_spacing" android:id="@androidprv:id/content_preview_file_layout"> - + android:scaleType="fitCenter" /> mFiles; private final ChooserContentPreviewUi.ActionFactory mActionFactory; - private final ImageLoader mImageLoader; private final HeadlineGenerator mHeadlineGenerator; FileContentPreviewUi( List files, ChooserContentPreviewUi.ActionFactory actionFactory, - ImageLoader imageLoader, HeadlineGenerator headlineGenerator) { mFiles = files; mActionFactory = actionFactory; - mImageLoader = imageLoader; mHeadlineGenerator = headlineGenerator; } @@ -82,30 +78,27 @@ class FileContentPreviewUi extends ContentPreviewUi { return contentPreviewLayout; } + FileInfo fileInfo = mFiles.get(0); + final CharSequence fileName; + final int iconId; if (uriCount == 1) { - loadFileUriIntoView(mFiles.get(0), contentPreviewLayout, mImageLoader); + fileName = fileInfo.getName(); + iconId = R.drawable.chooser_file_generic; } else { - FileInfo fileInfo = mFiles.get(0); int remUriCount = uriCount - 1; Map arguments = new HashMap<>(); arguments.put(PLURALS_COUNT, remUriCount); arguments.put(PLURALS_FILE_NAME, fileInfo.getName()); - String fileName = - PluralsMessageFormatter.format(resources, arguments, R.string.file_count); - - TextView fileNameView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_filename); - fileNameView.setText(fileName); - - View thumbnailView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_file_thumbnail); - thumbnailView.setVisibility(View.GONE); - - ImageView fileIconView = contentPreviewLayout.findViewById( - com.android.internal.R.id.content_preview_file_icon); - fileIconView.setVisibility(View.VISIBLE); - fileIconView.setImageResource(R.drawable.ic_file_copy); + fileName = PluralsMessageFormatter.format(resources, arguments, R.string.file_count); + iconId = R.drawable.ic_file_copy; } + TextView fileNameView = contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_filename); + fileNameView.setText(fileName); + + ImageView fileIconView = contentPreviewLayout.findViewById( + com.android.internal.R.id.content_preview_file_icon); + fileIconView.setImageResource(iconId); final ActionRow actionRow = contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); @@ -127,31 +120,4 @@ class FileContentPreviewUi extends ContentPreviewUi { } return actions; } - - private static void loadFileUriIntoView( - final FileInfo fileInfo, - final View parent, - final ImageLoader imageLoader) { - TextView fileNameView = parent.findViewById( - com.android.internal.R.id.content_preview_filename); - fileNameView.setText(fileInfo.getName()); - - if (fileInfo.getPreviewUri() != null) { - imageLoader.loadImage( - fileInfo.getPreviewUri(), - (bitmap) -> updateViewWithImage( - parent.findViewById( - com.android.internal.R.id.content_preview_file_thumbnail), - bitmap)); - } else { - View thumbnailView = parent.findViewById( - com.android.internal.R.id.content_preview_file_thumbnail); - thumbnailView.setVisibility(View.GONE); - - ImageView fileIconView = parent.findViewById( - com.android.internal.R.id.content_preview_file_icon); - fileIconView.setVisibility(View.VISIBLE); - fileIconView.setImageResource(R.drawable.chooser_file_generic); - } - } } -- cgit v1.2.3-59-g8ed1b From 8ae8c4a3f25cc5c0da3196fa0a3f0f07dfe87066 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Fri, 7 Apr 2023 17:54:25 -0700 Subject: Hide thumbnail prview if no preview has loaded In a case when a text is shared along with an image and image preview has failed to load, text exclusion from sharing does not hides the text but just greys it out (TextView marked disabled). This is to avoid unpleasant collapsing animation, something we'd need to address separately. Bug: 274643406 Test: manual testing Change-Id: I9fb9ce530afc1b31b2e075e15392bba256f36048 --- java/res/layout/chooser_grid_preview_image.xml | 9 +- java/res/values/dimens.xml | 1 + .../contentpreview/UnifiedContentPreviewUi.java | 15 ++- .../widget/ScrollableImagePreviewView.kt | 15 ++- .../ChooserActivityOverrideData.java | 5 +- .../intentresolver/ChooserWrapperActivity.java | 6 +- .../intentresolver/TestPreviewImageLoader.kt | 12 +- .../UnbundledChooserActivityTest.java | 131 +++++++++++++++++---- 8 files changed, 145 insertions(+), 49 deletions(-) (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 43f6f4d1..2cfab2a8 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -32,7 +32,9 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="center_horizontal" - android:layout_marginBottom="@dimen/chooser_view_spacing"> + android:layout_marginBottom="@dimen/chooser_view_spacing" + android:paddingStart="@dimen/chooser_edge_margin_normal_half" + android:paddingEnd="@dimen/chooser_edge_margin_normal_half"> 18dp 8dp 24dp + 12dp 20sp 1dp 120dp diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 748f7421..2e6ecb0e 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -91,6 +91,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { R.layout.chooser_grid_preview_image, parent, false); ScrollableImagePreviewView imagePreview = contentPreviewLayout.findViewById(R.id.scrollable_image_preview); + imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE)); final ActionRow actionRow = contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); @@ -132,7 +133,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mImageLoader); if (!TextUtils.isEmpty(mText) && mFiles.size() == 1 && allImages) { - setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory); + setTextInImagePreviewVisibility(contentPreviewLayout, imagePreview, mActionFactory); updateTextWithImageHeadline(contentPreviewLayout); } else { if (allImages) { @@ -175,7 +176,9 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { } private void setTextInImagePreviewVisibility( - ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) { + ViewGroup contentPreview, + ScrollableImagePreviewView imagePreview, + ChooserContentPreviewUi.ActionFactory actionFactory) { final TextView textView = contentPreview .requireViewById(com.android.internal.R.id.content_preview_text); CheckBox actionView = contentPreview @@ -194,8 +197,12 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { shareTextAction.accept(false); actionView.setOnCheckedChangeListener((view, isChecked) -> { view.setText(actionLabels[isChecked ? 1 : 0]); - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + textView.setEnabled(isChecked); + if (imagePreview.getVisibility() == View.VISIBLE) { + // animate only only if we have preview + TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); + textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); + } shareTextAction.accept(!isChecked); updateTextWithImageHeadline(contentPreview); }); diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index d1b0f5b4..8dcaacb8 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch @@ -129,13 +130,18 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { imageLoader, previews, otherItemCount, - ).apply { + ) { + onNoPreviewCallback?.run() + } + .apply { if (isMeasured) { loadAspectRatios(getMaxWidth(), this@ScrollableImagePreviewView::calcPreviewWidth) } } } + var onNoPreviewCallback: Runnable? = null + private fun getMaxWidth(): Int = when { maxWidthHint > 0 -> maxWidthHint @@ -187,6 +193,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private var totalItemCount: Int = 0 private val hasOtherItem get() = previews.size < totalItemCount + val hasPreviews: Boolean get() = previews.isNotEmpty() var transitionStatusElementCallback: TransitionElementStatusCallback? = null @@ -369,6 +376,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private val imageLoader: ImageLoader, previews: List, otherItemCount: Int, + private val onNoPreviewCallback: (() -> Unit) ) { private val pendingPreviews = ArrayDeque(previews) private val totalItemCount = previews.size + otherItemCount @@ -393,6 +401,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { reportFlow .takeWhile { it !== completedEvent } .throttle(ADAPTER_UPDATE_INTERVAL_MS) + .onCompletion { cause -> + if (cause == null && !adapter.hasPreviews) { + onNoPreviewCallback() + } + } .collect { if (isFirstUpdate) { isFirstUpdate = false diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index f9093b8f..2a4d654a 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -24,7 +24,6 @@ import static org.mockito.Mockito.when; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.Cursor; -import android.graphics.Bitmap; import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; @@ -64,7 +63,7 @@ public class ChooserActivityOverrideData { public boolean isImageType; public Cursor resolverCursor; public boolean resolverForceException; - public Bitmap previewThumbnail; + public ImageLoader imageLoader; public ChooserActivityLogger chooserActivityLogger; public int alternateProfileSetting; public Resources resources; @@ -83,7 +82,7 @@ public class ChooserActivityOverrideData { onSafelyStartInternalCallback = null; isVoiceInteraction = null; createPackageManager = null; - previewThumbnail = null; + imageLoader = null; isImageType = false; resolverCursor = null; resolverForceException = false; diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index 8886892f..dc9baade 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -194,9 +194,9 @@ public class ChooserWrapperActivity @Override protected ImageLoader createPreviewImageLoader() { - return new TestPreviewImageLoader( - super.createPreviewImageLoader(), - () -> sOverrides.previewThumbnail); + return sOverrides.imageLoader == null + ? super.createPreviewImageLoader() + : sOverrides.imageLoader; } @Override diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt index cfe041dd..2f240d58 100644 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -21,18 +21,12 @@ import android.net.Uri import java.util.function.Consumer internal class TestPreviewImageLoader( - private val imageLoader: ImageLoader, - private val imageOverride: () -> Bitmap? + private val bitmaps: Map ) : ImageLoader { override fun loadImage(uri: Uri, callback: Consumer) { - val override = imageOverride() - if (override != null) { - callback.accept(override) - } else { - imageLoader.loadImage(uri, callback) - } + callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(uri) + override suspend fun invoke(uri: Uri): Bitmap? = bitmaps[uri] override fun prePopulate(uris: List) = Unit } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 39357a4d..0a60b8c7 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -26,6 +26,7 @@ import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist; import static androidx.test.espresso.assertion.ViewAssertions.matches; import static androidx.test.espresso.matcher.ViewMatchers.hasSibling; import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.isEnabled; import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; @@ -125,6 +126,7 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -422,10 +424,12 @@ public class UnbundledChooserActivityTest { @Test public void visiblePreviewTitleAndThumbnail() throws InterruptedException { String previewTitle = "My Content Preview Title"; - Intent sendIntent = createSendTextIntentWithPreview(previewTitle, - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -688,10 +692,12 @@ public class UnbundledChooserActivityTest { @Test public void testImagePlusTextSharing_ExcludeText() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); @@ -728,10 +734,12 @@ public class UnbundledChooserActivityTest { @Test public void testImagePlusTextSharing_RemoveAndAddBackText() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; final String text = "https://google.com/search?q=google"; sendIntent.putExtra(Intent.EXTRA_TEXT, text); @@ -772,10 +780,12 @@ public class UnbundledChooserActivityTest { @Test public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); @@ -814,6 +824,42 @@ public class UnbundledChooserActivityTest { assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text); } + @Test + public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textRemainsVisible() { + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + ChooserActivityOverrideData.getInstance().isImageType = true; + sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); + + List resolvedComponentInfos = Arrays.asList( + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.imageviewer", "ImageTarget"), + sendIntent, PERSONAL_USER_HANDLE), + ResolverDataProvider.createResolvedComponentInfo( + new ComponentName("org.textviewer", "UriTarget"), + new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE) + ); + + setupResolverControllers(resolvedComponentInfos); + + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + + onView(withId(R.id.include_text_action)) + .check(matches(isDisplayed())) + .perform(click()); + waitForIdle(); + + onView(withId(R.id.scrollable_image_preview)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + onView(withId(com.android.internal.R.id.content_preview_text)) + .check(matches(allOf(isDisplayed(), not(isEnabled())))); + } + @Test public void copyTextToClipboard() throws Exception { Intent sendIntent = createSendTextIntent(); @@ -879,11 +925,12 @@ public class UnbundledChooserActivityTest { @Test @Ignore public void testEditImageLogs() throws Exception { - Intent sendIntent = createSendImageIntent( - Uri.parse("android.resource://com.android.frameworks.coretests/" - + R.drawable.test320x240)); - - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + Uri uri = Uri.parse( + "android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + Intent sendIntent = createSendImageIntent(uri); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -910,7 +957,8 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -932,6 +980,29 @@ public class UnbundledChooserActivityTest { }); } + @Test + public void allThumbnailsFailedToLoad_hidePreview() { + Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + + R.drawable.test320x240); + + ArrayList uris = new ArrayList<>(); + uris.add(uri); + uris.add(uri); + + Intent sendIntent = createSendUriIntentWithPreview(uris); + ChooserActivityOverrideData.getInstance().imageLoader = + new TestPreviewImageLoader(Collections.emptyMap()); + ChooserActivityOverrideData.getInstance().isImageType = true; + + List resolvedComponentInfos = createResolvedComponentsForTest(2); + + setupResolverControllers(resolvedComponentInfos); + mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); + waitForIdle(); + onView(withId(R.id.scrollable_image_preview)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + } + @Test public void testManyVisibleImagePreview_ScrollableImagePreview() { Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" @@ -950,7 +1021,8 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -980,7 +1052,8 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1004,7 +1077,8 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1093,7 +1167,8 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap(); + ChooserActivityOverrideData.getInstance().imageLoader = + createImageLoader(uri, createBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -2866,4 +2941,8 @@ public class UnbundledChooserActivityTest { }; return shortcutLoaders; } + + private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { + return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); + } } -- cgit v1.2.3-59-g8ed1b From 9c8b95a7e061b5e4970565f6b714962666aaedf6 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Tue, 4 Apr 2023 22:10:58 -0700 Subject: Calculare direct share expansion height only if epansion is enabled Plus remove duplicating spacing in image preview Fix: 275583356 Test: maunal testing Change-Id: I18eb1ee7eba76ca4cbb15a5600dafed579147358 --- java/res/layout/chooser_grid_preview_image.xml | 1 - java/src/com/android/intentresolver/ChooserActivity.java | 11 ++++++----- .../com/android/intentresolver/grid/ChooserGridAdapter.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) (limited to 'java/src') diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 43f6f4d1..4e1a2fc5 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -39,7 +39,6 @@ android:layout_width="wrap_content" android:layout_height="@dimen/chooser_preview_image_height_tall" android:layout_gravity="center_horizontal" - android:layout_marginBottom="@dimen/chooser_view_spacing" android:background="?android:attr/colorBackground" app:itemInnerSpacing="3dp" app:itemOuterSpacing="@dimen/chooser_edge_margin_normal" diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index dd0be4f0..dc9ba5ee 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -1512,11 +1512,12 @@ public class ChooserActivity extends ResolverActivity implements rowsToShow--; } - boolean isExpandable = getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode(); - if (directShareHeight != 0 && shouldShowContentPreview() - && isExpandable) { - // make sure to leave room for direct share 4->8 expansion + boolean isPortrait = getResources().getConfiguration().orientation + == Configuration.ORIENTATION_PORTRAIT; + boolean isExpandable = isPortrait && !isInMultiWindowMode() + && gridAdapter.canExpandDirectShare(); + if (directShareHeight != 0 && shouldShowContentPreview() && isExpandable) { + // make sure to leave room for direct share 4->8 expansion, if enabled int requiredExpansionHeight = (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE); int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0; diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java index 1cf59316..96f8c4d1 100644 --- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java +++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java @@ -581,7 +581,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter Date: Wed, 5 Apr 2023 08:11:09 -0700 Subject: Delete unused direct share expansion code ChooserGridAdapter#canExpandDirectShare was inlined everywhere and deleted; the code was trivially optimized and all methods that became unused has been deleted. A few IDE-suggested trivial changes along the way. Fix: 277015101 Test: manual testing Change-Id: I88462fde08842a348a5e4039a641a224476fa13e --- .../android/intentresolver/ChooserActivity.java | 91 +--------------------- .../intentresolver/grid/ChooserGridAdapter.java | 71 +++-------------- .../intentresolver/grid/DirectShareViewHolder.java | 87 +-------------------- 3 files changed, 15 insertions(+), 234 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index dc9ba5ee..75ee0648 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -57,7 +57,6 @@ import android.os.SystemClock; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; -import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; import android.util.Log; import android.util.Slog; @@ -88,7 +87,6 @@ import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; -import com.android.intentresolver.grid.DirectShareViewHolder; import com.android.intentresolver.model.AbstractResolverComparator; import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; @@ -96,7 +94,6 @@ import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.content.PackageMonitor; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -141,19 +138,11 @@ public class ChooserActivity extends ResolverActivity implements private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions"; - private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label"; - private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon"; - private static final boolean DEBUG = true; public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share"; private static final String SHORTCUT_TARGET = "shortcut_target"; - private static final String PLURALS_COUNT = "count"; - private static final String PLURALS_FILE_NAME = "file_name"; - - private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image"; - // TODO: these data structures are for one-time use in shuttling data from where they're // populated in `ShortcutToChooserTargetConverter` to where they're consumed in // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`. @@ -180,18 +169,6 @@ public class ChooserActivity extends ResolverActivity implements @Retention(RetentionPolicy.SOURCE) public @interface ShareTargetType {} - public static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f; - - 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 static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION - | Intent.FLAG_GRANT_WRITE_URI_PERMISSION - | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION - | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION; - private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the @@ -329,11 +306,6 @@ public class ChooserActivity extends ResolverActivity implements if (mResolverDrawerLayout != null) { mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); - // expand/shrink direct share 4 -> 8 viewgroup - if (mChooserRequest.isSendActionTarget()) { - mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll); - } - mResolverDrawerLayout.setOnCollapsedChangedListener( new ResolverDrawerLayout.OnCollapsedChangedListener() { @@ -1240,34 +1212,6 @@ public class ChooserActivity extends ResolverActivity implements mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick); ChooserActivity.this.updateProfileViewButton(); } - - @Override - public int getValidTargetCount() { - return mChooserMultiProfilePagerAdapter - .getActiveListAdapter() - .getSelectableServiceTargetCount(); - } - - @Override - public void updateDirectShareExpansion(DirectShareViewHolder directShareGroup) { - RecyclerView activeAdapterView = - mChooserMultiProfilePagerAdapter.getActiveAdapterView(); - if (mResolverDrawerLayout.isCollapsed()) { - directShareGroup.collapse(activeAdapterView); - } else { - directShareGroup.expand(activeAdapterView); - } - } - - @Override - public void handleScrollToExpandDirectShare( - DirectShareViewHolder directShareGroup, int y, int oldy) { - directShareGroup.handleScroll( - mChooserMultiProfilePagerAdapter.getActiveAdapterView(), - y, - oldy, - mMaxTargetsPerRow); - } }, chooserListAdapter, shouldShowContentPreview(), @@ -1381,12 +1325,6 @@ public class ChooserActivity extends ResolverActivity implements }); } - private void handleScroll(View view, int x, int y, int oldx, int oldy) { - if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) { - mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy); - } - } - /* * Need to dynamically adjust how many icons can fit per row before we add them, * which also means setting the correct offset to initially show the content @@ -1455,9 +1393,7 @@ public class ChooserActivity extends ResolverActivity implements private int calculateDrawerOffset( int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) { - final int bottomInset = mSystemWindowInsets != null - ? mSystemWindowInsets.bottom : 0; - int offset = bottomInset; + int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0; int rowsToShow = gridAdapter.getSystemRowCount() + gridAdapter.getProfileRowCount() + gridAdapter.getServiceTargetRowCount() @@ -1487,7 +1423,6 @@ public class ChooserActivity extends ResolverActivity implements } if (recyclerView.getVisibility() == View.VISIBLE) { - int directShareHeight = 0; rowsToShow = Math.min(4, rowsToShow); boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow); mLastNumberOfChildren = recyclerView.getChildCount(); @@ -1503,29 +1438,8 @@ public class ChooserActivity extends ResolverActivity implements if (shouldShowExtraRow) { offset += height; } - - if (gridAdapter.getTargetType( - recyclerView.getChildAdapterPosition(child)) - == ChooserListAdapter.TARGET_SERVICE) { - directShareHeight = height; - } rowsToShow--; } - - boolean isPortrait = getResources().getConfiguration().orientation - == Configuration.ORIENTATION_PORTRAIT; - boolean isExpandable = isPortrait && !isInMultiWindowMode() - && gridAdapter.canExpandDirectShare(); - if (directShareHeight != 0 && shouldShowContentPreview() && isExpandable) { - // make sure to leave room for direct share 4->8 expansion, if enabled - int requiredExpansionHeight = - (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE); - int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0; - int minHeight = bottom - top - mResolverDrawerLayout.getAlwaysShowHeight() - - requiredExpansionHeight - topInset - bottomInset; - - offset = Math.min(offset, minHeight); - } } else { ViewGroup currentEmptyStateView = getActiveEmptyStateView(); if (currentEmptyStateView.getVisibility() == View.VISIBLE) { @@ -1825,9 +1739,6 @@ public class ChooserActivity extends ResolverActivity implements @Override protected void onProfileTabSelected() { - ChooserGridAdapter currentRootAdapter = - mChooserMultiProfilePagerAdapter.getCurrentRootAdapter(); - currentRootAdapter.updateDirectShareExpansion(); // This fixes an edge case where after performing a variety of gestures, vertical scrolling // ends up disabled. That's because at some point the old tab's vertical scrolling is // disabled and the new tab's is enabled. For context, see b/159997845 diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java index 96f8c4d1..e6f70d4f 100644 --- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java +++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java @@ -89,26 +89,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter mRows; - private int mCellCountPerRow; + private final int mCellCountPerRow; - private boolean mHideDirectShareExpansion = false; private int mDirectShareMinHeight = 0; private int mDirectShareCurrHeight = 0; - private int mDirectShareMaxHeight = 0; private final boolean[] mCellVisibility; - private final Supplier mDeferredTargetCountSupplier; - public DirectShareViewHolder( ViewGroup parent, List rows, int cellCountPerRow, - int viewType, - Supplier deferredTargetCountSupplier) { + int viewType) { super(rows.size() * cellCountPerRow, parent, viewType); this.mParent = parent; @@ -61,7 +51,6 @@ public class DirectShareViewHolder extends ItemGroupViewHolder { this.mCellCountPerRow = cellCountPerRow; this.mCellVisibility = new boolean[rows.size() * cellCountPerRow]; Arrays.fill(mCellVisibility, true); - this.mDeferredTargetCountSupplier = deferredTargetCountSupplier; } public ViewGroup addView(int index, View v) { @@ -92,7 +81,6 @@ public class DirectShareViewHolder extends ItemGroupViewHolder { mDirectShareMinHeight = getRow(0).getMeasuredHeight(); mDirectShareCurrHeight = (mDirectShareCurrHeight > 0) ? mDirectShareCurrHeight : mDirectShareMinHeight; - mDirectShareMaxHeight = 2 * mDirectShareMinHeight; } public int getMeasuredRowHeight() { @@ -123,75 +111,4 @@ public class DirectShareViewHolder extends ItemGroupViewHolder { fadeAnim.start(); } } - - public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) { - // only exit early if fully collapsed, otherwise onListRebuilt() with shifting - // targets can lock us into an expanded mode - boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight; - if (notExpanded) { - if (mHideDirectShareExpansion) { - return; - } - - // only expand if we have more than maxTargetsPerRow, and delay that decision - // until they start to scroll - final int validTargets = this.mDeferredTargetCountSupplier.get(); - if (validTargets <= maxTargetsPerRow) { - mHideDirectShareExpansion = true; - return; - } - } - - int yDiff = (int) ((oldy - y) * ChooserActivity.DIRECT_SHARE_EXPANSION_RATE); - - int prevHeight = mDirectShareCurrHeight; - int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight); - newHeight = Math.max(newHeight, mDirectShareMinHeight); - yDiff = newHeight - prevHeight; - - updateDirectShareRowHeight(view, yDiff, newHeight); - } - - public void expand(RecyclerView view) { - updateDirectShareRowHeight( - view, mDirectShareMaxHeight - mDirectShareCurrHeight, mDirectShareMaxHeight); - } - - public void collapse(RecyclerView view) { - updateDirectShareRowHeight( - view, mDirectShareMinHeight - mDirectShareCurrHeight, mDirectShareMinHeight); - } - - private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) { - if (view == null || view.getChildCount() == 0 || yDiff == 0) { - return; - } - - // locate the item to expand, and offset the rows below that one - boolean foundExpansion = false; - for (int i = 0; i < view.getChildCount(); i++) { - View child = view.getChildAt(i); - - if (foundExpansion) { - child.offsetTopAndBottom(yDiff); - } else { - if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) { - int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(), - MeasureSpec.EXACTLY); - int heightSpec = MeasureSpec.makeMeasureSpec(newHeight, - MeasureSpec.EXACTLY); - child.measure(widthSpec, heightSpec); - child.getLayoutParams().height = child.getMeasuredHeight(); - child.layout(child.getLeft(), child.getTop(), child.getRight(), - child.getTop() + child.getMeasuredHeight()); - - foundExpansion = true; - } - } - } - - if (foundExpansion) { - mDirectShareCurrHeight = newHeight; - } - } } -- cgit v1.2.3-59-g8ed1b From 7d348309828270bbf4cb0f9b3d9b93600cf14a7c Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Thu, 13 Apr 2023 21:08:38 -0700 Subject: Show edit action onfor for a single image preview Bug: 277629860 Test: manual test Change-Id: I5fa70e9105bc1716af64b3c22f292308ec9ca6f0 --- .../intentresolver/contentpreview/UnifiedContentPreviewUi.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 2e6ecb0e..9ce875c8 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -158,9 +158,11 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { if (action != null) { actions.add(action); } - action = mActionFactory.createEditButton(); - if (action != null) { - actions.add(action); + if (mFiles.size() == 1 && mTypeClassifier.isImageType(mFiles.get(0).getMimeType())) { + action = mActionFactory.createEditButton(); + if (action != null) { + actions.add(action); + } } return actions; } -- cgit v1.2.3-59-g8ed1b From f0b4a9f9c194618c0eb87edf7ecad48f3583e8f9 Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Fri, 14 Apr 2023 12:37:50 -0700 Subject: Fix image edit transition animation Bug: 278282157 Test: manual testing Change-Id: Id5d5d91d01c209bf94850d4c8e72d98cf6a9fa5e --- .../android/intentresolver/ChooserActionFactory.java | 3 --- .../src/com/android/intentresolver/ChooserActivity.java | 8 +++++--- .../android/intentresolver/widget/ImagePreviewView.kt | 2 ++ .../intentresolver/widget/ScrollableImagePreviewView.kt | 17 ++++++++++++++--- .../android/intentresolver/ChooserActionFactoryTest.kt | 3 --- 5 files changed, 21 insertions(+), 12 deletions(-) (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 8dafae86..23e04560 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -40,7 +40,6 @@ import android.view.View; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; -import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.widget.ActionRow; import com.android.internal.annotations.VisibleForTesting; @@ -104,7 +103,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio /** * @param context * @param chooserRequest data about the invocation of the current Sharesheet session. - * @param featureFlagRepository feature flags that may control the eligibility of some actions. * @param integratedDeviceComponents info about other components that are available on this * device to implement the supported action types. * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text" @@ -118,7 +116,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio public ChooserActionFactory( Context context, ChooserRequestParameters chooserRequest, - FeatureFlagRepository featureFlagRepository, ChooserIntegratedDeviceComponents integratedDeviceComponents, ChooserActivityLogger logger, Consumer onUpdateSharedTextIsExcluded, diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 75ee0648..404d6da3 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -92,6 +92,7 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.shortcuts.AppPredictorFactory; import com.android.intentresolver.shortcuts.ShortcutLoader; +import com.android.intentresolver.widget.ImagePreviewView; import com.android.intentresolver.widget.ResolverDrawerLayout; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.PackageMonitor; @@ -682,8 +683,10 @@ public class ChooserActivity extends ResolverActivity implements @Nullable private View getFirstVisibleImgPreviewView() { - View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large); - return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null; + View imagePreview = findViewById(R.id.scrollable_image_preview); + return imagePreview instanceof ImagePreviewView + ? ((ImagePreviewView) imagePreview).getTransitionView() + : null; } /** @@ -1295,7 +1298,6 @@ public class ChooserActivity extends ResolverActivity implements return new ChooserActionFactory( this, mChooserRequest, - mFeatureFlagRepository, mIntegratedDeviceComponents, getChooserActivityLogger(), (isExcluded) -> mExcludeSharedText = isExcluded, diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt index 8813adca..5f92b149 100644 --- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt @@ -18,11 +18,13 @@ package com.android.intentresolver.widget import android.graphics.Bitmap import android.net.Uri +import android.view.View internal typealias ImageLoader = suspend (Uri) -> Bitmap? interface ImagePreviewView { fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) + fun getTransitionView(): View? /** * [ImagePreviewView] progressively prepares views for shared element transition and reports diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 8dcaacb8..7755610d 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -122,6 +122,15 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { previewAdapter.transitionStatusElementCallback = callback } + override fun getTransitionView(): View? { + for (i in 0 until childCount) { + val child = getChildAt(i) + val vh = getChildViewHolder(child) + if (vh is PreviewViewHolder && vh.image.transitionName != null) return child + } + return null + } + fun setPreviews(previews: List, otherItemCount: Int, imageLoader: ImageLoader) { previewAdapter.reset(0, imageLoader) batchLoader?.cancel() @@ -250,7 +259,8 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { is PreviewViewHolder -> vh.bind( previews[position], imageLoader ?: error("ImageLoader is missing"), - if (position == firstImagePos && transitionStatusElementCallback != null) { + isSharedTransitionElement = position == firstImagePos, + previewReadyCallback = if (position == firstImagePos && transitionStatusElementCallback != null) { this::onTransitionElementReady } else { null @@ -282,7 +292,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } private class PreviewViewHolder(view: View) : ViewHolder(view) { - private val image = view.requireViewById(R.id.image) + val image = view.requireViewById(R.id.image) private val badgeFrame = view.requireViewById(R.id.badge_frame) private val badge = view.requireViewById(R.id.badge) private var scope: CoroutineScope? = null @@ -290,13 +300,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { fun bind( preview: Preview, imageLoader: ImageLoader, + isSharedTransitionElement: Boolean, previewReadyCallback: ((String) -> Unit)? ) { image.setImageDrawable(null) (image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params -> params.dimensionRatio = preview.aspectRatioString } - image.transitionName = if (previewReadyCallback != null) { + image.transitionName = if (isSharedTransitionElement) { TRANSITION_NAME } else { null diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt index 0a8c22b7..d72c9aa6 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -29,11 +29,9 @@ import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.android.intentresolver.flags.FeatureFlagRepository -import com.android.intentresolver.flags.Flags import com.google.common.collect.ImmutableList import com.google.common.truth.Truth.assertThat import org.junit.After -import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -142,7 +140,6 @@ class ChooserActionFactoryTest { return ChooserActionFactory( context, chooserRequest, - flags, mock(), logger, Consumer{}, -- cgit v1.2.3-59-g8ed1b From 5c05904fbda5a7d8b0b3a1d474ab7224adb686ee Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Wed, 22 Mar 2023 15:12:43 -0700 Subject: Remove bitmap caching from ScrollableImagePreview Remove bitmap caching from ScrollableImagePreview; instead, extend the image loader interface to proivde a cache-control option. ScrollableImagePreview$BatchPreviewLoader requests caching only for the first visible items as we'd like them to be displayed as fast as possible and not be evicted by any of the remaining items. Minor fixes inside ImagePreviewImageLoader. ImageLoader is moved into contentpreview package. Bug: 271613784 Test: unit tests Test: Manual testing with injected extensive logging. Change-Id: Ic07572e1fb73d589d98207af8fe58ae52698802a --- .../android/intentresolver/ChooserActivity.java | 5 +- java/src/com/android/intentresolver/ImageLoader.kt | 26 ------ .../intentresolver/ImagePreviewImageLoader.kt | 80 ++++++++++++++++--- .../contentpreview/ChooserContentPreviewUi.java | 1 - .../intentresolver/contentpreview/ImageLoader.kt | 51 ++++++++++++ .../contentpreview/TextContentPreviewUi.java | 1 - .../contentpreview/UnifiedContentPreviewUi.java | 1 - .../intentresolver/widget/ImagePreviewView.kt | 4 - .../widget/ScrollableImagePreviewView.kt | 29 +++---- .../ChooserActivityOverrideData.java | 1 + .../intentresolver/ChooserWrapperActivity.java | 1 + .../intentresolver/ImagePreviewImageLoaderTest.kt | 93 +++++++++++++++++++++- .../intentresolver/TestPreviewImageLoader.kt | 4 +- .../UnbundledChooserActivityTest.java | 1 + .../contentpreview/ChooserContentPreviewUiTest.kt | 3 +- 15 files changed, 230 insertions(+), 71 deletions(-) delete mode 100644 java/src/com/android/intentresolver/ImageLoader.kt create mode 100644 java/src/com/android/intentresolver/contentpreview/ImageLoader.kt (limited to 'java/src') diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 404d6da3..917a4e5d 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -84,6 +84,7 @@ import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; @@ -1287,9 +1288,9 @@ public class ChooserActivity extends ResolverActivity implements protected ImageLoader createPreviewImageLoader() { final int cacheSize; float chooserWidth = getResources().getDimension(R.dimen.chooser_width); - // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView) + // imageWidth = imagePreviewHeight * minAspectRatio (see ScrollableImagePreviewView) float imageWidth = - getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2; + getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 2 / 5; cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize); } diff --git a/java/src/com/android/intentresolver/ImageLoader.kt b/java/src/com/android/intentresolver/ImageLoader.kt deleted file mode 100644 index 0ed8b122..00000000 --- a/java/src/com/android/intentresolver/ImageLoader.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2022 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 - -import android.graphics.Bitmap -import android.net.Uri -import java.util.function.Consumer - -interface ImageLoader : suspend (Uri) -> Bitmap? { - fun loadImage(uri: Uri, callback: Consumer) - fun prePopulate(uris: List) -} diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt index 9650403e..c97efdd1 100644 --- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt @@ -26,8 +26,11 @@ import androidx.annotation.VisibleForTesting import androidx.collection.LruCache import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope +import com.android.intentresolver.contentpreview.ImageLoader +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -35,6 +38,10 @@ import java.util.function.Consumer private const val TAG = "ImagePreviewImageLoader" +/** + * Implements preview image loading for the content preview UI. Provides requests deduplication and + * image caching. + */ @VisibleForTesting class ImagePreviewImageLoader @JvmOverloads constructor( private val context: Context, @@ -48,14 +55,17 @@ class ImagePreviewImageLoader @JvmOverloads constructor( Size(it, it) } - @GuardedBy("self") - private val cache = LruCache>(cacheSize) + private val lock = Any() + @GuardedBy("lock") + private val cache = LruCache(cacheSize) + @GuardedBy("lock") + private val runningRequests = HashMap() - override suspend fun invoke(uri: Uri): Bitmap? = loadImageAsync(uri) + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) override fun loadImage(uri: Uri, callback: Consumer) { lifecycle.coroutineScope.launch { - val image = loadImageAsync(uri) + val image = loadImageAsync(uri, caching = true) if (isActive) { callback.accept(image) } @@ -65,23 +75,44 @@ class ImagePreviewImageLoader @JvmOverloads constructor( override fun prePopulate(uris: List) { uris.asSequence().take(cache.maxSize()).forEach { uri -> lifecycle.coroutineScope.launch { - loadImageAsync(uri) + loadImageAsync(uri, caching = true) } } } - private suspend fun loadImageAsync(uri: Uri): Bitmap? { - return synchronized(cache) { - cache.get(uri) ?: CompletableDeferred().also { result -> - cache.put(uri, result) - lifecycle.coroutineScope.launch(dispatcher) { - result.loadBitmap(uri) + private suspend fun loadImageAsync(uri: Uri, caching: Boolean): Bitmap? { + return getRequestDeferred(uri, caching) + .await() + } + + private fun getRequestDeferred(uri: Uri, caching: Boolean): Deferred { + var shouldLaunchImageLoading = false + val request = synchronized(lock) { + cache[uri] + ?: runningRequests.getOrPut(uri) { + shouldLaunchImageLoading = true + RequestRecord(uri, CompletableDeferred(), caching) + }.apply { + this.caching = this.caching || caching } + } + if (shouldLaunchImageLoading) { + request.loadBitmapAsync() + } + return request.deferred + } + + private fun RequestRecord.loadBitmapAsync() { + lifecycle.coroutineScope.launch(dispatcher) { + loadBitmap() + }.invokeOnCompletion { cause -> + if (cause is CancellationException) { + cancel() } - }.await() + } } - private fun CompletableDeferred.loadBitmap(uri: Uri) { + private fun RequestRecord.loadBitmap() { val bitmap = try { context.contentResolver.loadThumbnail(uri, thumbnailSize, null) } catch (t: Throwable) { @@ -90,4 +121,27 @@ class ImagePreviewImageLoader @JvmOverloads constructor( } complete(bitmap) } + + private fun RequestRecord.cancel() { + synchronized(lock) { + runningRequests.remove(uri) + deferred.cancel() + } + } + + private fun RequestRecord.complete(bitmap: Bitmap?) { + deferred.complete(bitmap) + synchronized(lock) { + runningRequests.remove(uri) + if (bitmap != null && caching) { + cache.put(uri, this) + } + } + } + + private class RequestRecord( + val uri: Uri, + val deferred: CompletableDeferred, + @GuardedBy("lock") var caching: Boolean + ) } diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 56027a16..181fe117 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -38,7 +38,6 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt new file mode 100644 index 00000000..225807ee --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -0,0 +1,51 @@ +/* + * 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.contentpreview + +import android.graphics.Bitmap +import android.net.Uri +import java.util.function.Consumer + +/** + * A content preview image loader. + */ +interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? { + /** + * Load preview image asynchronously; caching is allowed. + * @param uri content URI + * @param callback a callback that will be invoked with the loaded image or null if loading has + * failed. + */ + fun loadImage(uri: Uri, callback: Consumer) + + /** + * Prepopulate the image loader cache. + */ + fun prePopulate(uris: List) + + /** + * Load preview image; caching is allowed. + */ + override suspend fun invoke(uri: Uri) = invoke(uri, true) + + /** + * Load preview image. + * @param uri content URI + * @param caching indicates if the loaded image could be cached. + */ + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? +} diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index ece0c312..6bf9a1cc 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -27,7 +27,6 @@ import android.widget.TextView; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 9ce875c8..709ec566 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -31,7 +31,6 @@ import android.widget.TextView; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt index 5f92b149..3f0458ee 100644 --- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt @@ -16,12 +16,8 @@ package com.android.intentresolver.widget -import android.graphics.Bitmap -import android.net.Uri import android.view.View -internal typealias ImageLoader = suspend (Uri) -> Bitmap? - interface ImagePreviewView { fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) fun getTransitionView(): View? diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 7755610d..524b4f81 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -56,6 +56,8 @@ private const val MIN_ASPECT_RATIO_STRING = "2:5" private const val MAX_ASPECT_RATIO = 2.5f private const val MAX_ASPECT_RATIO_STRING = "5:2" +private typealias CachingImageLoader = suspend (Uri, Boolean) -> Bitmap? + class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) @@ -131,7 +133,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { return null } - fun setPreviews(previews: List, otherItemCount: Int, imageLoader: ImageLoader) { + fun setPreviews(previews: List, otherItemCount: Int, imageLoader: CachingImageLoader) { previewAdapter.reset(0, imageLoader) batchLoader?.cancel() batchLoader = BatchPreviewLoader( @@ -176,8 +178,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { ) { constructor(type: PreviewType, uri: Uri) : this(type, uri, "1:1") - internal var bitmap: Bitmap? = null - internal fun updateAspectRatio(width: Int, height: Int) { if (width <= 0 || height <= 0) return val aspectRatio = width.toFloat() / height.toFloat() @@ -197,7 +197,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private val context: Context ) : RecyclerView.Adapter() { private val previews = ArrayList() - private var imageLoader: ImageLoader? = null + private var imageLoader: CachingImageLoader? = null private var firstImagePos = -1 private var totalItemCount: Int = 0 @@ -206,7 +206,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { var transitionStatusElementCallback: TransitionElementStatusCallback? = null - fun reset(totalItemCount: Int, imageLoader: ImageLoader) { + fun reset(totalItemCount: Int, imageLoader: CachingImageLoader) { this.imageLoader = imageLoader firstImagePos = -1 previews.clear() @@ -299,7 +299,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { fun bind( preview: Preview, - imageLoader: ImageLoader, + imageLoader: CachingImageLoader, isSharedTransitionElement: Boolean, previewReadyCallback: ((String) -> Unit)? ) { @@ -334,11 +334,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - private suspend fun loadImage(preview: Preview, imageLoader: ImageLoader) { - val bitmap = preview.bitmap ?: runCatching { + private suspend fun loadImage(preview: Preview, imageLoader: CachingImageLoader) { + val bitmap = runCatching { // it's expected for all loading/caching optimizations to be implemented by the // loader - imageLoader(preview.uri) + imageLoader(preview.uri, true) }.getOrNull() image.setImageBitmap(bitmap) } @@ -384,7 +384,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private class BatchPreviewLoader( private val adapter: Adapter, - private val imageLoader: ImageLoader, + private val imageLoader: CachingImageLoader, previews: List, otherItemCount: Int, private val onNoPreviewCallback: (() -> Unit) @@ -435,18 +435,15 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { launch { while (pendingPreviews.isNotEmpty()) { val preview = pendingPreviews.poll() ?: continue + val isVisible = loadedPreviewWidth < maxWidth val bitmap = runCatching { // TODO: decide on adding a timeout - imageLoader(preview.uri) + imageLoader(preview.uri, isVisible) }.getOrNull() ?: continue preview.updateAspectRatio(bitmap.width, bitmap.height) updates.add(preview) - if (loadedPreviewWidth < maxWidth) { + if (isVisible) { loadedPreviewWidth += previewWidthCalculator(bitmap) - // cache bitmaps for the first preview items to aovid potential - // double-loading (in case those values are evicted from the image - // loader's cache) - preview.bitmap = bitmap if (loadedPreviewWidth >= maxWidth) { // notify that the preview now can be displayed reportFlow.emit(updateEvent) diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index 2a4d654a..9ebeb79d 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -28,6 +28,7 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.shortcuts.ShortcutLoader; diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index dc9baade..d23e4a66 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -35,6 +35,7 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.shortcuts.ShortcutLoader; diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt index f327e19e..3c399cc4 100644 --- a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt @@ -19,11 +19,18 @@ package com.android.intentresolver import android.content.ContentResolver import android.content.Context import android.content.res.Resources +import android.graphics.Bitmap import android.net.Uri import android.util.Size import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain @@ -41,7 +48,10 @@ class ImagePreviewImageLoaderTest { private val imageSize = Size(300, 300) private val uriOne = Uri.parse("content://org.package.app/image-1.png") private val uriTwo = Uri.parse("content://org.package.app/image-2.png") - private val contentResolver = mock() + private val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val contentResolver = mock { + whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap) + } private val resources = mock { whenever(getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)) .thenReturn(imageSize.width) @@ -70,7 +80,7 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_prePopulate() = runTest { + fun prePopulate_cachesImagesUpToTheCacheSize() = runTest { testSubject.prePopulate(listOf(uriOne, uriTwo)) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) @@ -81,7 +91,7 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_invoke_return_cached_image() = runTest { + fun invoke_returnCachedImageWhenCalledTwice() = runTest { testSubject(uriOne) testSubject(uriOne) @@ -89,7 +99,33 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_invoke_old_records_evicted_from_the_cache() = runTest { + fun invoke_whenInstructed_doesNotCache() = runTest { + testSubject(uriOne, false) + testSubject(uriOne, false) + + verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_overlappedRequests_Deduplicate() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + scheduler.advanceUntilIdle() + } + + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_oldRecordsEvictedFromTheCache() = runTest { testSubject(uriOne) testSubject(uriTwo) testSubject(uriTwo) @@ -98,4 +134,53 @@ class ImagePreviewImageLoaderTest { verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) } + + @Test + fun invoke_doNotCacheNulls() = runTest { + whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) + testSubject(uriOne) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + } + + @Test(expected = CancellationException::class) + fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest { + lifecycleOwner.state = Lifecycle.State.DESTROYED + testSubject(uriOne) + } + + @Test(expected = CancellationException::class) + fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + val deferred = async(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + lifecycleOwner.state = Lifecycle.State.DESTROYED + scheduler.advanceUntilIdle() + deferred.await() + } + } + + @Test + fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + launch(start = UNDISPATCHED) { + testSubject(uriOne, true) + } + scheduler.advanceUntilIdle() + } + testSubject(uriOne, true) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } } diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt index 2f240d58..74a253b8 100644 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -18,6 +18,7 @@ package com.android.intentresolver import android.graphics.Bitmap import android.net.Uri +import com.android.intentresolver.contentpreview.ImageLoader import java.util.function.Consumer internal class TestPreviewImageLoader( @@ -27,6 +28,7 @@ internal class TestPreviewImageLoader( callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri): Bitmap? = bitmaps[uri] + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + override fun prePopulate(uris: List) = Unit } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 0a60b8c7..de5498db 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -104,6 +104,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 7b9a0ce6..8eec289e 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -21,7 +21,6 @@ import android.content.ContentInterface import android.content.Intent import android.graphics.Bitmap import android.net.Uri -import com.android.intentresolver.ImageLoader import com.android.intentresolver.any import com.android.intentresolver.anyOrNull import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory @@ -48,7 +47,7 @@ class ChooserContentPreviewUiTest { callback.accept(null) } override fun prePopulate(uris: List) = Unit - override suspend fun invoke(uri: Uri): Bitmap? = null + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = null } private val actionFactory = object : ActionFactory { override fun createCopyButton() = ActionRow.Action(label = "Copy", icon = null) {} -- cgit v1.2.3-59-g8ed1b From b1c6bf9d8100906355d113986304f2b8997dba20 Mon Sep 17 00:00:00 2001 From: 1 Date: Tue, 11 Apr 2023 20:52:04 +0000 Subject: Show preview content in landscape. Make stuff smaller and less tall on landscape non-tablets. Let the system restart sharesheet on rotation to allow for resources to change. Tablet displays in landscape generally should follow the un-compressed UI of portrait phones, so the resources for that configuration have to be listed twice. This doesn't dismantle any of the code for handling orientation changes ourselves, as taking that apart may be error-prone. This is meant to a first pass, additional landscape miniturization will follow, as there still isn't a ton of room to scroll at the bottom. Bug: 267522604 Test: Testing in portrait and landscape with a variety of ShareTest payloads Change-Id: I4697ad9510526cc9ce5f9e0630080bef5fad377c --- AndroidManifest.xml | 2 +- java/res/layout/chooser_action_button.xml | 31 --------- java/res/layout/chooser_action_row.xml | 4 +- java/res/layout/chooser_grid_preview_image.xml | 2 +- java/res/layout/chooser_grid_preview_text.xml | 2 +- java/res/values-h480dp/bools.xml | 20 ------ java/res/values-land/bools.xml | 20 ++++++ java/res/values-land/dimens.xml | 4 ++ java/res/values-land/integers.xml | 19 +++++ java/res/values-sw600dp/bools.xml | 20 ++++++ java/res/values-sw600dp/dimens.xml | 5 +- java/res/values-sw600dp/integers.xml | 19 +++++ java/res/values/attrs.xml | 4 ++ java/res/values/bools.xml | 2 +- java/res/values/dimens.xml | 2 - java/res/values/integers.xml | 19 +++++ .../android/intentresolver/ChooserActivity.java | 3 +- .../intentresolver/widget/ChooserActionRow.kt | 81 ---------------------- .../intentresolver/widget/ScrollableActionRow.kt | 21 ++++-- 19 files changed, 133 insertions(+), 147 deletions(-) delete mode 100644 java/res/layout/chooser_action_button.xml delete mode 100644 java/res/values-h480dp/bools.xml create mode 100644 java/res/values-land/bools.xml create mode 100644 java/res/values-land/integers.xml create mode 100644 java/res/values-sw600dp/bools.xml create mode 100644 java/res/values-sw600dp/integers.xml create mode 100644 java/res/values/integers.xml delete mode 100644 java/src/com/android/intentresolver/widget/ChooserActionRow.kt (limited to 'java/src') diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5228827d..8115b2b2 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -56,7 +56,7 @@ android:excludeFromRecents="true" android:documentLaunchMode="never" android:relinquishTaskIdentity="true" - android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden" + android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden" android:visibleToInstantApps="true" android:exported="true" android:enabled="false"> diff --git a/java/res/layout/chooser_action_button.xml b/java/res/layout/chooser_action_button.xml deleted file mode 100644 index 2b68ccca..00000000 --- a/java/res/layout/chooser_action_button.xml +++ /dev/null @@ -1,31 +0,0 @@ - - -