/*
* Copyright (C) 2024 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 static android.app.VoiceInteractor.PickOptionRequest.Option;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
import static com.android.intentresolver.ChooserActionFactory.EDIT_SOURCE;
import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL;
import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK;
import static com.android.intentresolver.ui.model.ActivityModel.ACTIVITY_MODEL_KEY;
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import static java.util.Objects.requireNonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.VoiceInteractor;
import android.app.admin.DevicePolicyEventLogger;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
import android.os.StrictMode;
import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.service.chooser.ChooserTarget;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.android.intentresolver.ChooserRefinementManager.RefinementType;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.contentpreview.PreviewViewModel;
import com.android.intentresolver.data.model.ChooserRequest;
import com.android.intentresolver.data.repository.DevicePolicyResources;
import com.android.intentresolver.domain.interactor.UserInteractor;
import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider;
import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.Caching;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.intentresolver.inject.Background;
import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.measurements.Tracer;
import com.android.intentresolver.model.AbstractResolverComparator;
import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.platform.AppPredictionAvailable;
import com.android.intentresolver.platform.ImageEditor;
import com.android.intentresolver.platform.NearbyShare;
import com.android.intentresolver.profiles.ChooserMultiProfilePagerAdapter;
import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType;
import com.android.intentresolver.profiles.OnProfileSelectedListener;
import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener;
import com.android.intentresolver.profiles.TabConfig;
import com.android.intentresolver.shared.model.Profile;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.intentresolver.ui.ActionTitle;
import com.android.intentresolver.ui.ProfilePagerResources;
import com.android.intentresolver.ui.ShareResultSender;
import com.android.intentresolver.ui.ShareResultSenderFactory;
import com.android.intentresolver.ui.model.ActivityModel;
import com.android.intentresolver.ui.viewmodel.ChooserViewModel;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.LatencyTracker;
import com.google.common.collect.ImmutableList;
import dagger.hilt.android.AndroidEntryPoint;
import kotlin.Pair;
import kotlinx.coroutines.CoroutineDispatcher;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.inject.Provider;
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@AndroidEntryPoint(FragmentActivity.class)
public class ChooserActivity extends Hilt_ChooserActivity implements
ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem {
private static final String TAG = "ChooserActivity";
/**
* Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
* in onStop when launched in a new task. If this extra is set to true, we do not finish
* ourselves when onStop gets called.
*/
public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
= "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
/**
* Transition name for the first image preview.
* To be used for shared element transition into this activity.
*/
public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
private static final boolean DEBUG = true;
public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
private static final String SHORTCUT_TARGET = "shortcut_target";
//////////////////////////////////////////////////////////////////////////////////////////////
// Inherited properties.
//////////////////////////////////////////////////////////////////////////////////////////////
private static final String TAB_TAG_PERSONAL = "personal";
private static final String TAB_TAG_WORK = "work";
private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
public static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
private int mLayoutId;
private UserHandle mHeaderCreatorUser;
private boolean mRegistered;
private PackageMonitor mPersonalPackageMonitor;
private PackageMonitor mWorkPackageMonitor;
protected ResolverDrawerLayout mResolverDrawerLayout;
private TabHost mTabHost;
private ResolverViewPager mViewPager;
protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
protected final LatencyTracker mLatencyTracker = getLatencyTracker();
/** See {@link #setRetainInOnStop}. */
private boolean mRetainInOnStop;
protected Insets mSystemWindowInsets = null;
private ResolverActivity.PickTargetOptionRequest mPickOptionRequest;
@Nullable
private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
// 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`.
// That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
// intermediate data, and then these members can be removed.
private final Maptrue
if the activity is finishing and creation should halt.
*/
private boolean configureContentView(TargetDataLoader targetDataLoader) {
if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null) {
throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() "
+ "cannot be null.");
}
Trace.beginSection("configureContentView");
// We partially rebuild the inactive adapter to determine if we should auto launch
// isTabLoaded will be true here if the empty state screen is shown instead of the list.
boolean rebuildCompleted = mChooserMultiProfilePagerAdapter.rebuildTabs(
mProfiles.getWorkProfilePresent());
mLayoutId = R.layout.chooser_grid_scrollable_preview;
setContentView(mLayoutId);
mTabHost = findViewById(com.android.internal.R.id.profile_tabhost);
mViewPager = requireViewById(com.android.internal.R.id.profile_pager);
mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager);
boolean result = postRebuildList(rebuildCompleted);
Trace.endSection();
return result;
}
/**
* Finishing procedures to be performed after the list has been rebuilt.
*
true
if the activity is finishing and creation should halt.
*/
protected boolean postRebuildList(boolean rebuildCompleted) {
return postRebuildListInternal(rebuildCompleted);
}
/**
* Add a label to signify that the user can pick a different app.
* @param adapter The adapter used to provide data to item views.
*/
public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
final boolean useHeader = adapter.hasFilteredItem();
if (useHeader) {
FrameLayout stub = findViewById(com.android.internal.R.id.stub);
stub.setVisibility(View.VISIBLE);
TextView textView = (TextView) LayoutInflater.from(this).inflate(
R.layout.resolver_different_item_header, null, false);
if (mProfiles.getWorkProfilePresent()) {
textView.setGravity(Gravity.CENTER);
}
stub.addView(textView);
}
}
private void setupViewVisibilities() {
ChooserListAdapter activeListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
if (!mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
addUseDifferentAppLabelIfNecessary(activeListAdapter);
}
}
/**
* Finishing procedures to be performed after the list has been rebuilt.
* @param rebuildCompleted
* @return true
if the activity is finishing and creation should halt.
*/
final boolean postRebuildListInternal(boolean rebuildCompleted) {
int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
// We only rebuild asynchronously when we have multiple elements to sort. In the case where
// we're already done, we can check if we should auto-launch immediately.
if (rebuildCompleted && maybeAutolaunchActivity()) {
return true;
}
setupViewVisibilities();
if (mProfiles.getWorkProfilePresent()
|| (mProfiles.getPrivateProfilePresent()
&& mProfileAvailability.isAvailable(
requireNonNull(mProfiles.getPrivateProfile())))) {
setupProfileTabs();
}
return false;
}
private void setupProfileTabs() {
mChooserMultiProfilePagerAdapter.setupProfileTabs(
getLayoutInflater(),
mTabHost,
mViewPager,
R.layout.resolver_profile_tab_button,
com.android.internal.R.id.profile_pager,
() -> onProfileTabSelected(mViewPager.getCurrentItem()),
new OnProfileSelectedListener() {
@Override
public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {}
@Override
public void onProfilePageStateChanged(int state) {
onHorizontalSwipeStateChanged(state);
}
});
mOnSwitchOnWorkSelectedListener = () -> {
View workTab = mTabHost.getTabWidget().getChildAt(
mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK));
workTab.setFocusable(true);
workTab.setFocusableInTouchMode(true);
workTab.requestFocus();
};
}
//////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
UserHandle mainUserHandle = mProfiles.getPersonalHandle();
ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
if (record.shortcutLoader == null) {
Tracer.INSTANCE.endLaunchToShortcutTrace();
}
UserHandle workUserHandle = mProfiles.getWorkHandle();
if (workUserHandle != null) {
createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
UserHandle privateUserHandle = mProfiles.getPrivateHandle();
if (privateUserHandle != null && mProfileAvailability.isAvailable(
requireNonNull(mProfiles.getPrivateProfile()))) {
createProfileRecord(privateUserHandle, targetIntentFilter, factory);
}
}
private ProfileRecord createProfileRecord(
UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
AppPredictor appPredictor = factory.create(userHandle);
ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
? null
: createShortcutLoader(
this,
appPredictor,
userHandle,
targetIntentFilter,
shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader);
mProfileRecords.put(userHandle.getIdentifier(), record);
return record;
}
@Nullable
private ProfileRecord getProfileRecord(UserHandle userHandle) {
return mProfileRecords.get(userHandle.getIdentifier());
}
@VisibleForTesting
protected ShortcutLoader createShortcutLoader(
Context context,
AppPredictor appPredictor,
UserHandle userHandle,
IntentFilter targetIntentFilter,
ConsumerIf {@code listAdapter} is {@code null}, both profile list adapters are updated if
* available.
*/
private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) {
// Refresh pinned items
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
if (listAdapter == null) {
mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs();
} else {
listAdapter.handlePackagesChanged();
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
if (mSystemWindowInsets != null) {
mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
mSystemWindowInsets.right, 0);
}
if (mViewPager.isLayoutRtl()) {
mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager);
}
mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
adjustPreviewWidth(newConfig.orientation, null);
updateStickyContentPreview();
updateTabPadding();
}
private boolean shouldDisplayLandscape(int orientation) {
// Sharesheet fixes the # of items per row and therefore can not correctly lay out
// when in the restricted size of multi-window mode. In the future, would be nice
// to use minimum dp size requirements instead
return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
}
private void adjustPreviewWidth(int orientation, View parent) {
int width = -1;
if (mShouldDisplayLandscape) {
width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
}
parent = parent == null ? getWindow().getDecorView() : parent;
updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
}
private void updateTabPadding() {
if (mProfiles.getWorkProfilePresent()) {
View tabs = findViewById(com.android.internal.R.id.tabs);
float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
// The entire width consists of icons or padding. Divide the item padding in half to get
// paddingHorizontal.
float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
/ mMaxTargetsPerRow / 2;
// Subtract the margin the buttons already have.
padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
tabs.setPadding((int) padding, 0, (int) padding, 0);
}
}
private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
View view = parent.findViewById(layoutResourceId);
if (view != null && view.getLayoutParams() != null) {
LayoutParams params = view.getLayoutParams();
params.width = width;
view.setLayoutParams(params);
}
}
/**
* Create a view that will be shown in the content preview area
* @param parent reference to the parent container where the view should be attached to
* @return content preview view
*/
protected ViewGroup createContentPreviewView(ViewGroup parent) {
ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
getResources(),
getLayoutInflater(),
parent,
requireViewById(R.id.chooser_headline_row_container));
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
}
return layout;
}
@Nullable
private View getFirstVisibleImgPreviewView() {
View imagePreview = findViewById(R.id.scrollable_image_preview);
return imagePreview instanceof ImagePreviewView
? ((ImagePreviewView) imagePreview).getTransitionView()
: null;
}
/**
* Wrapping the ContentResolver call to expose for easier mocking,
* and to avoid mocking Android core classes.
*/
@VisibleForTesting
public Cursor queryResolver(ContentResolver resolver, Uri uri) {
return resolver.query(uri, null, null, null, null);
}
private void destroyProfileRecords() {
mProfileRecords.values().forEach(ProfileRecord::destroy);
mProfileRecords.clear();
}
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
Intent result = defIntent;
if (mRequest.getReplacementExtras() != null) {
final Bundle replExtras =
mRequest.getReplacementExtras().getBundle(aInfo.packageName);
if (replExtras != null) {
result = new Intent(defIntent);
result.putExtras(replExtras);
}
}
if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
|| aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
result = Intent.createChooser(result,
getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
// Don't auto-launch single intents if the intent is being forwarded. This is done
// because automatically launching a resolving application as a response to the user
// action of switching accounts is pretty unexpected.
result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
}
return result;
}
private void maybeSendShareResult(TargetInfo cti) {
if (mShareResultSender != null) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo());
}
}
}
private void addCallerChooserTargets() {
if (!mRequest.getCallerChooserTargets().isEmpty()) {
// Send the caller's chooser targets only to the default profile.
if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
/* origTarget */ null,
new ArrayList<>(mRequest.getCallerChooserTargets()),
TARGET_TYPE_DEFAULT,
/* directShareShortcutInfoCache */ Collections.emptyMap(),
/* directShareAppTargetCache */ Collections.emptyMap());
}
}
}
@Override // ResolverListCommunicator
public boolean shouldGetActivityMetadata() {
return true;
}
public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
if (target.isSuspended()) {
return false;
}
// TODO: migrate to ChooserRequest
return mViewModel.getActivityModel().getIntent()
.getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
}
private void showTargetDetails(TargetInfo targetInfo) {
if (targetInfo == null) return;
List