blob: f49afbce5226753bfa7872ef6ad44ccebfbcb782 [file] [log] [blame]
// Copyright 2019 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.app.Activity;
import android.content.res.Configuration;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.appcompat.content.res.AppCompatResources;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.Callback;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.base.metrics.RecordUserAction;
import org.chromium.base.supplier.Supplier;
import org.chromium.base.task.PostTask;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
import org.chromium.chrome.browser.lifecycle.ActivityLifecycleDispatcher;
import org.chromium.chrome.browser.lifecycle.ConfigurationChangedObserver;
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;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextCoordinator.LinkGeneration;
import org.chromium.chrome.browser.share.link_to_text.LinkToTextMetricsHelper;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.modules.image_editor.ImageEditorModuleProvider;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetContent;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetController;
import org.chromium.components.browser_ui.bottomsheet.BottomSheetObserver;
import org.chromium.components.browser_ui.bottomsheet.EmptyBottomSheetObserver;
import org.chromium.components.browser_ui.settings.SettingsLauncher;
import org.chromium.components.browser_ui.share.ShareParams;
import org.chromium.components.favicon.LargeIconBridge;
import org.chromium.components.feature_engagement.Tracker;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.base.WindowAndroid.ActivityStateObserver;
import org.chromium.ui.modelutil.PropertyModel;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
/**
* Coordinator for displaying the share sheet.
*/
// TODO(crbug/1022172): Should be package-protected once modularization is complete.
public class ShareSheetCoordinator implements ActivityStateObserver, ChromeOptionShareCallback,
ConfigurationChangedObserver,
View.OnLayoutChangeListener {
private final BottomSheetController mBottomSheetController;
private final Supplier<Tab> mTabProvider;
private final ShareSheetPropertyModelBuilder mPropertyModelBuilder;
private final Callback<Tab> mPrintTabCallback;
private final SettingsLauncher mSettingsLauncher;
private final boolean mIsSyncEnabled;
private final ImageEditorModuleProvider mImageEditorModuleProvider;
private long mShareStartTime;
private boolean mExcludeFirstParty;
private boolean mIsMultiWindow;
private Set<Integer> mContentTypes;
private Activity mActivity;
private ActivityLifecycleDispatcher mLifecycleDispatcher;
private ChromeProvidedSharingOptionsProvider mChromeProvidedSharingOptionsProvider;
private ShareParams mShareParams;
private ShareSheetBottomSheetContent mBottomSheet;
private WindowAndroid mWindowAndroid;
private ChromeShareExtras mChromeShareExtras;
private LinkToTextCoordinator mLinkToTextCoordinator;
private final BottomSheetObserver mBottomSheetObserver;
private final LargeIconBridge mIconBridge;
private final Tracker mFeatureEngagementTracker;
private @LinkGeneration int mLinkGenerationStatusForMetrics = LinkGeneration.MAX;
/**
* Constructs a new ShareSheetCoordinator.
*
* @param controller The {@link BottomSheetController} for the current activity.
* @param lifecycleDispatcher Dispatcher for activity lifecycle events, e.g. configuration
* changes.
* @param tabProvider Supplier for the current activity tab.
* @param modelBuilder The {@link ShareSheetPropertyModelBuilder} for the share sheet.
* @param imageEditorModuleProvider Image Editor module entry point if present in the APK.
*/
// TODO(crbug/1022172): Should be package-protected once modularization is complete.
public ShareSheetCoordinator(BottomSheetController controller,
ActivityLifecycleDispatcher lifecycleDispatcher, Supplier<Tab> tabProvider,
ShareSheetPropertyModelBuilder modelBuilder, Callback<Tab> printTab,
LargeIconBridge iconBridge, SettingsLauncher settingsLauncher, boolean isSyncEnabled,
ImageEditorModuleProvider imageEditorModuleProvider, Tracker featureEngagementTracker) {
mBottomSheetController = controller;
mLifecycleDispatcher = lifecycleDispatcher;
mLifecycleDispatcher.register(this);
mTabProvider = tabProvider;
mPropertyModelBuilder = modelBuilder;
mPrintTabCallback = printTab;
mSettingsLauncher = settingsLauncher;
mIsSyncEnabled = isSyncEnabled;
mImageEditorModuleProvider = imageEditorModuleProvider;
mBottomSheetObserver = new EmptyBottomSheetObserver() {
@Override
public void onSheetContentChanged(BottomSheetContent bottomSheet) {
super.onSheetContentChanged(bottomSheet);
if (bottomSheet == mBottomSheet) {
mBottomSheet.getContentView().addOnLayoutChangeListener(
ShareSheetCoordinator.this::onLayoutChange);
} else {
mBottomSheet.getContentView().removeOnLayoutChangeListener(
ShareSheetCoordinator.this::onLayoutChange);
}
}
};
mBottomSheetController.addObserver(mBottomSheetObserver);
mIconBridge = iconBridge;
mFeatureEngagementTracker = featureEngagementTracker;
}
protected void destroy() {
if (mShareParams != null) {
ShareParams.TargetChosenCallback callback = mShareParams.getCallback();
if (callback != null) {
callback.onCancel();
}
}
if (mWindowAndroid != null) {
mWindowAndroid.removeActivityStateObserver(this);
mWindowAndroid = null;
}
if (mLifecycleDispatcher != null) {
mLifecycleDispatcher.unregister(this);
mLifecycleDispatcher = null;
}
}
// TODO(crbug/1022172): Should be package-protected once modularization is complete.
@Override
public void showShareSheet(
ShareParams params, ChromeShareExtras chromeShareExtras, long shareStartTime) {
mShareParams = params;
mChromeShareExtras = chromeShareExtras;
mActivity = params.getWindow().getActivity().get();
if (mActivity == null) return;
if (mWindowAndroid == null) {
mWindowAndroid = params.getWindow();
if (mWindowAndroid != null) {
mWindowAndroid.addActivityStateObserver(this);
}
}
mBottomSheet = new ShareSheetBottomSheetContent(mActivity, mIconBridge, this, params);
mShareStartTime = shareStartTime;
if (ChromeFeatureList.isEnabled(ChromeFeatureList.PREEMPTIVE_LINK_TO_TEXT_GENERATION)) {
mLinkGenerationStatusForMetrics = mBottomSheet.getLinkGenerationState();
}
updateShareSheet(this::finishShowShareSheet);
}
/**
* Updates {@code mShareParams} from the {@link LinkGeneration} state.
* Called when toggling between LinkToText options
*
* @param state The state from {@link LinkGeneration} to which ShareParams should be updated.
*/
void updateShareSheetForLinkToText(@LinkGeneration int state) {
if (!ChromeFeatureList.isEnabled(ChromeFeatureList.PREEMPTIVE_LINK_TO_TEXT_GENERATION)
|| mLinkToTextCoordinator == null) {
return;
}
mShareParams = mLinkToTextCoordinator.getShareParams(state);
mBottomSheet.updateShareParams(mShareParams);
mLinkGenerationStatusForMetrics = state;
updateShareSheet(null);
}
private void updateShareSheet(Runnable onUpdateFinished) {
mContentTypes =
ShareSheetPropertyModelBuilder.getContentTypes(mShareParams, mChromeShareExtras);
List<PropertyModel> firstPartyApps = createFirstPartyPropertyModels(
mActivity, mShareParams, mChromeShareExtras, mContentTypes);
createThirdPartyPropertyModels(mActivity, mShareParams, mContentTypes,
mChromeShareExtras.saveLastUsed(), thirdPartyApps -> {
finishUpdateShareSheet(firstPartyApps, thirdPartyApps, onUpdateFinished);
});
}
private void finishUpdateShareSheet(List<PropertyModel> firstPartyApps,
List<PropertyModel> thirdPartyApps, @Nullable Runnable onUpdateFinished) {
mBottomSheet.createRecyclerViews(
firstPartyApps, thirdPartyApps, mContentTypes, mShareParams.getFileContentType());
if (onUpdateFinished != null) {
onUpdateFinished.run();
}
}
private void finishShowShareSheet() {
boolean shown = mBottomSheetController.requestShowContent(mBottomSheet, true);
if (shown) {
long delta = System.currentTimeMillis() - mShareStartTime;
RecordHistogram.recordMediumTimesHistogram(
"Sharing.SharingHubAndroid.TimeToShowShareSheet", delta);
}
}
/**
* If preemptive link to text generation is enable, create LinkTotextCoordinator
* which will generate link to text, create a new share and show share sheet.
* Otherwise show share sheet with the current share.
*
* @param params The {@link ShareParams} for the current share.
* @param chromeShareExtras The {@link ChromeShareExtras} for the current share.
* @param shareStartTime The start time of the current share.
*/
public void showInitialShareSheet(
ShareParams params, ChromeShareExtras chromeShareExtras, long shareStartTime) {
if (ChromeFeatureList.isEnabled(ChromeFeatureList.PREEMPTIVE_LINK_TO_TEXT_GENERATION)
&& chromeShareExtras.isUserHighlightedText()) {
if (!chromeShareExtras.isReshareHighlightedText()) {
LinkToTextMetricsHelper.recordLinkToTextDiagnoseStatus(
LinkToTextMetricsHelper.LinkToTextDiagnoseStatus
.SHOW_SHARINGHUB_FOR_HIGHLIGHT);
}
String tabUrl =
mTabProvider.get().isInitialized() ? mTabProvider.get().getUrl().getSpec() : "";
mLinkToTextCoordinator =
new LinkToTextCoordinator(params, mTabProvider.get(), this, chromeShareExtras,
shareStartTime, getUrlToShare(params, chromeShareExtras, tabUrl));
return;
}
showShareSheet(params, chromeShareExtras, shareStartTime);
}
// Used by first party features to share with only non-chrome apps.
@Override
public void showThirdPartyShareSheet(
ShareParams params, ChromeShareExtras chromeShareExtras, long shareStartTime) {
mExcludeFirstParty = true;
showShareSheet(params, chromeShareExtras, shareStartTime);
}
List<PropertyModel> createFirstPartyPropertyModels(Activity activity, ShareParams shareParams,
ChromeShareExtras chromeShareExtras, Set<Integer> contentTypes) {
if (mExcludeFirstParty) {
return new ArrayList<>();
}
mChromeProvidedSharingOptionsProvider = new ChromeProvidedSharingOptionsProvider(activity,
mTabProvider, mBottomSheetController, mBottomSheet, shareParams, mPrintTabCallback,
mSettingsLauncher, mIsSyncEnabled, mShareStartTime, this,
mImageEditorModuleProvider, mFeatureEngagementTracker,
getUrlToShare(shareParams, chromeShareExtras,
mTabProvider.get().isInitialized() ? mTabProvider.get().getUrl().getSpec()
: ""),
mLinkGenerationStatusForMetrics);
mIsMultiWindow = ApiCompatibilityUtils.isInMultiWindowMode(activity);
return mChromeProvidedSharingOptionsProvider.getPropertyModels(
contentTypes, mIsMultiWindow);
}
/**
* Create third-party property models.
*
* <p>
* This method delivers its result asynchronously through {@code callback},
* to allow for the upcoming ShareRanking backend, which is asynchronous.
* The existing backend is synchronous, but this method is an asynchronous
* wrapper around it so that the design of the rest of this class won't need
* to change when ShareRanking is hooked up.
* TODO(https://crbug.com/1217186)
* </p>
*/
@VisibleForTesting
void createThirdPartyPropertyModels(Activity activity, ShareParams params,
Set<Integer> contentTypes, boolean saveLastUsed,
Callback<List<PropertyModel>> callback) {
if (params == null) {
PostTask.postTask(UiThreadTaskTraits.DEFAULT, callback.bind(null));
return;
}
List<PropertyModel> models =
mPropertyModelBuilder.selectThirdPartyApps(mBottomSheet, contentTypes, params,
saveLastUsed, mShareStartTime, mLinkGenerationStatusForMetrics);
// More...
PropertyModel morePropertyModel = ShareSheetPropertyModelBuilder.createPropertyModel(
AppCompatResources.getDrawable(activity, R.drawable.sharing_more),
activity.getResources().getString(R.string.sharing_more_icon_label),
(shareParams)
-> {
RecordUserAction.record("SharingHubAndroid.MoreSelected");
if (ChromeFeatureList.isEnabled(
ChromeFeatureList.PREEMPTIVE_LINK_TO_TEXT_GENERATION)) {
LinkToTextMetricsHelper.recordSharedHighlightStateMetrics(
mLinkGenerationStatusForMetrics);
}
mBottomSheetController.hideContent(mBottomSheet, true);
Profile profile = null;
if (mTabProvider.get() != null && mTabProvider.get().getWebContents() != null) {
profile = Profile.fromWebContents(mTabProvider.get().getWebContents());
}
ShareHelper.showDefaultShareUi(params, profile, saveLastUsed);
// Reset callback to prevent cancel() being called when the custom sheet is
// closed. The callback will be called by ShareHelper on actions from the
// default share UI.
params.setCallback(null);
},
/*displayNew*/ false);
models.add(morePropertyModel);
PostTask.postTask(UiThreadTaskTraits.DEFAULT, callback.bind(models));
}
@VisibleForTesting
protected void disableFirstPartyFeaturesForTesting() {
mExcludeFirstParty = true;
}
/**
* Returns the url to share.
*
* <p>This prioritizes the URL in {@link ShareParams}, but if it does not exist, we look for an
* image source URL from {@link ChromeShareExtras}. The image source URL is not contained in
* {@link ShareParams#getUrl()} because we do not want to share the image URL with the image
* file in third-party app shares. If both are empty then current tab URL is used. This is
* useful for {@link LinkToTextCoordinator} that needs URL but it cannot be provided through
* {@link ShareParams}.
*/
private String getUrlToShare(
ShareParams shareParams, ChromeShareExtras chromeShareExtras, String tabUrl) {
if (!TextUtils.isEmpty(shareParams.getUrl())) {
return shareParams.getUrl();
} else if (!chromeShareExtras.getImageSrcUrl().isEmpty()) {
return chromeShareExtras.getImageSrcUrl().getSpec();
}
return tabUrl;
}
// ActivityStateObserver
@Override
public void onActivityResumed() {}
@Override
public void onActivityPaused() {
if (mBottomSheet != null) {
mBottomSheetController.hideContent(mBottomSheet, true);
}
}
@Override
public void onActivityDestroyed() {}
// ConfigurationChangedObserver
@Override
public void onConfigurationChanged(Configuration newConfig) {
if (mActivity == null) {
return;
}
boolean isMultiWindow = ApiCompatibilityUtils.isInMultiWindowMode(mActivity);
// mContentTypes is null if Chrome features should not be shown.
if (mIsMultiWindow == isMultiWindow || mContentTypes == null) {
return;
}
mIsMultiWindow = isMultiWindow;
mBottomSheet.createFirstPartyRecyclerViews(
mChromeProvidedSharingOptionsProvider.getPropertyModels(
mContentTypes, mIsMultiWindow));
mBottomSheetController.requestShowContent(mBottomSheet, /*animate=*/false);
}
// View.OnLayoutChangeListener
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
if ((oldRight - oldLeft) == (right - left)) {
return;
}
mBottomSheet.getFirstPartyView().invalidate();
mBottomSheet.getFirstPartyView().requestLayout();
mBottomSheet.getThirdPartyView().invalidate();
mBottomSheet.getThirdPartyView().requestLayout();
}
}