blob: e915decdf86d0bdb4cef6dad1de0815607d49419 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.share.share_sheet;
import android.content.ComponentName;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.View.OnClickListener;
import androidx.annotation.IntDef;
import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.profiles.Profile;
import org.chromium.chrome.browser.share.ChromeShareExtras;
import org.chromium.chrome.browser.share.ShareHelper;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextCoordinator.LinkGeneration;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextMetricsHelper;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.ui.modelutil.PropertyModel;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Handles displaying the share sheet. The version used depends on several conditions:
*
* <ul>
* <li>Android K and below: custom share dialog
* <li>Android L+: system share sheet
* <li>#chrome-sharing-hub enabled: custom share sheet
* </ul>
*/
// TODO(crbug/1022172): Should be package-protected once modularization is complete.
public class ShareSheetPropertyModelBuilder {
@IntDef({ContentType.LINK_PAGE_VISIBLE, ContentType.LINK_PAGE_NOT_VISIBLE, ContentType.TEXT,
ContentType.HIGHLIGHTED_TEXT, ContentType.LINK_AND_TEXT, ContentType.IMAGE,
ContentType.OTHER_FILE_TYPE, ContentType.IMAGE_AND_LINK})
@Retention(RetentionPolicy.SOURCE)
@interface ContentType {
int LINK_PAGE_VISIBLE = 0;
int LINK_PAGE_NOT_VISIBLE = 1;
int TEXT = 2;
int HIGHLIGHTED_TEXT = 3;
int LINK_AND_TEXT = 4;
int IMAGE = 5;
int OTHER_FILE_TYPE = 6;
int IMAGE_AND_LINK = 7;
}
private static final int MAX_NUM_APPS = 7;
private static final String IMAGE_TYPE = "image/";
// Variations parameter name for the comma-separated list of third-party activity names.
private static final String PARAM_SHARING_HUB_THIRD_PARTY_APPS = "sharing-hub-third-party-apps";
static final HashSet<Integer> ALL_CONTENT_TYPES_FOR_TEST = new HashSet<>(
Arrays.asList(ContentType.LINK_PAGE_VISIBLE, ContentType.LINK_PAGE_NOT_VISIBLE,
ContentType.TEXT, ContentType.HIGHLIGHTED_TEXT, ContentType.LINK_AND_TEXT,
ContentType.IMAGE, ContentType.OTHER_FILE_TYPE, ContentType.IMAGE_AND_LINK));
private static final ArrayList<String> FALLBACK_ACTIVITIES =
new ArrayList<>(Arrays.asList("com.whatsapp.ContactPicker",
"com.facebook.composer.shareintent.ImplicitShareIntentHandlerDefaultAlias",
"com.google.android.gm.ComposeActivityGmailExternal",
"com.instagram.share.handleractivity.StoryShareHandlerActivity",
"com.facebook.messenger.intents.ShareIntentHandler",
"com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity",
"com.twitter.composer.ComposerActivity", "com.snap.mushroom.MainActivity",
"com.pinterest.activity.create.PinItActivity",
"com.linkedin.android.publishing.sharing.LinkedInDeepLinkActivity",
"jp.naver.line.android.activity.selectchat.SelectChatActivityLaunchActivity",
"com.facebook.lite.composer.activities.ShareIntentMultiPhotoAlphabeticalAlias",
"com.facebook.mlite.share.view.ShareActivity",
"com.samsung.android.email.ui.messageview.MessageFileView",
"com.yahoo.mail.ui.activities.ComposeActivity",
"org.telegram.ui.LaunchActivity", "com.tencent.mm.ui.tools.ShareImgUI"));
private final BottomSheetController mBottomSheetController;
private final PackageManager mPackageManager;
private final Profile mProfile;
// TODO(crbug/1022172): Should be package-protected once modularization is complete.
public ShareSheetPropertyModelBuilder(BottomSheetController bottomSheetController,
PackageManager packageManager, Profile profile) {
mBottomSheetController = bottomSheetController;
mPackageManager = packageManager;
mProfile = profile;
}
/**
* Returns a set of {@link ContentType}s for the current share.
*
* Adds {@link ContentType}s according to the following logic:
*
* <ul>
* <li>If a URL is present, {@code isUrlOfVisiblePage} determines whether to add
* {@link ContentType.LINK_PAGE_VISIBLE} or {@link ContentType.LINK_PAGE_NOT_VISIBLE}.
* <li>If the text being shared is not the same as the URL, add {@link ContentType.TEXT}
* <li>If text is highlighted by user, add {@link ContentType.HIGHLIGHTED_TEXT}.
* <li>If the share contains files and the {@code fileContentType} is an image, add
* {@link ContentType.IMAGE}. Otherwise, add {@link ContentType.OTHER_FILE_TYPE}.
* </ul>
*/
static Set<Integer> getContentTypes(ShareParams params, ChromeShareExtras chromeShareExtras) {
Set<Integer> contentTypes = new HashSet<>();
boolean hasUrl = !TextUtils.isEmpty(params.getUrl());
if (hasUrl && !chromeShareExtras.skipPageSharingActions()) {
if (chromeShareExtras.isUrlOfVisiblePage()) {
contentTypes.add(ContentType.LINK_PAGE_VISIBLE);
} else {
contentTypes.add(ContentType.LINK_PAGE_NOT_VISIBLE);
}
}
if (!TextUtils.isEmpty(params.getText())) {
if (chromeShareExtras.isUserHighlightedText()) {
contentTypes.add(ContentType.HIGHLIGHTED_TEXT);
} else {
contentTypes.add(ContentType.TEXT);
}
}
if (hasUrl && !TextUtils.isEmpty(params.getText())) {
contentTypes.add(ContentType.LINK_AND_TEXT);
}
if (params.getFileUris() != null) {
if (!TextUtils.isEmpty(params.getFileContentType())
&& params.getFileContentType().startsWith(IMAGE_TYPE)) {
if (hasUrl) {
contentTypes.add(ContentType.IMAGE_AND_LINK);
} else {
contentTypes.add(ContentType.IMAGE);
}
} else {
contentTypes.add(ContentType.OTHER_FILE_TYPE);
}
}
return contentTypes;
}
protected List<PropertyModel> selectThirdPartyApps(ShareSheetBottomSheetContent bottomSheet,
Set<Integer> contentTypes, ShareParams params, boolean saveLastUsed,
long shareStartTime, @LinkGeneration int linkGenerationStatusForMetrics) {
List<String> thirdPartyActivityNames = getThirdPartyActivityNames();
List<ResolveInfo> resolveInfoList =
getCompatibleApps(contentTypes, params.getFileContentType());
List<ResolveInfo> thirdPartyActivities = new ArrayList<>();
// Construct a list of 3P apps. The list should be sorted by the country-specific
// ranking when available or the fallback list defined above. If less than MAX_NUM_APPS
// are available the list is filled with whatever else is available.
for (String s : thirdPartyActivityNames) {
for (ResolveInfo res : resolveInfoList) {
if (res.activityInfo.name.equals(s)) {
thirdPartyActivities.add(res);
resolveInfoList.remove(res);
break;
}
}
if (thirdPartyActivities.size() == MAX_NUM_APPS) {
break;
}
}
String chromePackageName = ContextUtils.getApplicationContext().getPackageName();
for (ResolveInfo res : resolveInfoList) {
if (!res.activityInfo.packageName.equals(chromePackageName)) {
thirdPartyActivities.add(res);
}
if (thirdPartyActivities.size() == MAX_NUM_APPS) {
break;
}
}
ArrayList<PropertyModel> models = new ArrayList<>();
for (int i = 0; i < MAX_NUM_APPS && i < thirdPartyActivities.size(); ++i) {
ResolveInfo info = thirdPartyActivities.get(i);
final int logIndex = i;
OnClickListener onClickListener = v
-> onThirdPartyAppSelected(bottomSheet, params, saveLastUsed, info.activityInfo,
logIndex, shareStartTime, linkGenerationStatusForMetrics);
PropertyModel propertyModel =
createPropertyModel(ShareHelper.loadIconForResolveInfo(info, mPackageManager),
(String) info.loadLabel(mPackageManager), onClickListener,
/*displayNew*/ false);
models.add(propertyModel);
}
return models;
}
private void onThirdPartyAppSelected(ShareSheetBottomSheetContent bottomSheet,
ShareParams params, boolean saveLastUsed, ActivityInfo ai, int logIndex,
long shareStartTime, @LinkGeneration int linkGenerationStatusForMetrics) {
// Record all metrics.
RecordUserAction.record("SharingHubAndroid.ThirdPartyAppSelected");
RecordHistogram.recordEnumeratedHistogram(
"Sharing.SharingHubAndroid.ThirdPartyAppUsage", logIndex, MAX_NUM_APPS + 1);
ChromeProvidedSharingOptionsProvider.recordTimeToShare(shareStartTime);
if (ChromeFeatureList.isEnabled(ChromeFeatureList.PREEMPTIVE_LINK_TO_TEXT_GENERATION)) {
LinkToTextMetricsHelper.recordSharedHighlightStateMetrics(
linkGenerationStatusForMetrics);
}
ComponentName component = new ComponentName(ai.applicationInfo.packageName, ai.name);
ShareParams.TargetChosenCallback callback = params.getCallback();
if (callback != null) {
callback.onTargetChosen(component);
// Reset callback after onTargetChosen() is called to prevent cancel() being called when
// the sheet is closed.
params.setCallback(null);
}
if (saveLastUsed) {
ShareHelper.setLastShareComponentName(mProfile, component);
}
mBottomSheetController.hideContent(bottomSheet, true);
// Fire intent through ShareHelper.
ShareHelper.shareDirectly(params, component);
}
/**
* Returns a list of compatible {@link ResolveInfo}s for the set of {@link ContentType}s.
*
* Adds {@link ResolveInfo}s according to the following logic:
*
* <ul>
* <li>If the {@link ContentType}s contain URL or Text, add text-sharing apps.
* <li>If the {@link ContentType}s contain a file, add file-sharing apps compatible
* {@code fileContentType}.
* </ul>
*/
private List<ResolveInfo> getCompatibleApps(Set<Integer> contentTypes, String fileContentType) {
List<ResolveInfo> resolveInfoList = new ArrayList<>();
if (!Collections.disjoint(contentTypes,
Arrays.asList(ContentType.LINK_PAGE_NOT_VISIBLE, ContentType.LINK_PAGE_VISIBLE,
ContentType.TEXT, ContentType.HIGHLIGHTED_TEXT))) {
resolveInfoList.addAll(mPackageManager.queryIntentActivities(
ShareHelper.getShareLinkAppCompatibilityIntent(), 0));
}
if (!Collections.disjoint(contentTypes,
Arrays.asList(ContentType.IMAGE, ContentType.IMAGE_AND_LINK,
ContentType.OTHER_FILE_TYPE))) {
resolveInfoList.addAll(mPackageManager.queryIntentActivities(
ShareHelper.createShareFileAppCompatibilityIntent(fileContentType), 0));
}
return resolveInfoList;
}
static PropertyModel createPropertyModel(
Drawable icon, String label, OnClickListener listener, boolean showNewBadge) {
return new PropertyModel.Builder(ShareSheetItemViewProperties.ALL_KEYS)
.with(ShareSheetItemViewProperties.ICON, icon)
.with(ShareSheetItemViewProperties.LABEL, label)
.with(ShareSheetItemViewProperties.CLICK_LISTENER, listener)
.with(ShareSheetItemViewProperties.SHOW_NEW_BADGE, showNewBadge)
.build();
}
private List<String> getThirdPartyActivityNames() {
String param = ChromeFeatureList.getFieldTrialParamByFeature(
ChromeFeatureList.CHROME_SHARING_HUB, PARAM_SHARING_HUB_THIRD_PARTY_APPS);
if (param.isEmpty()) {
return FALLBACK_ACTIVITIES;
}
return new ArrayList<>(Arrays.asList(param.split(",")));
}
}