/*
* Copyright (C) 2008 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.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.ext.CreationExtrasExtKt.addDefaultArgs;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
import static java.util.Objects.requireNonNull;
import android.app.ActivityThread;
import android.app.VoiceInteractor.PickOptionRequest;
import android.app.VoiceInteractor.PickOptionRequest.Option;
import android.app.VoiceInteractor.Prompt;
import android.app.admin.DevicePolicyEventLogger;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.PatternMatcher;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
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.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.Space;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.viewpager.widget.ViewPager;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
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.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.intentresolver.inject.Background;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.profiles.MultiProfilePagerAdapter;
import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType;
import com.android.intentresolver.profiles.OnProfileSelectedListener;
import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener;
import com.android.intentresolver.profiles.ResolverMultiProfilePagerAdapter;
import com.android.intentresolver.profiles.TabConfig;
import com.android.intentresolver.shared.model.Profile;
import com.android.intentresolver.ui.ActionTitle;
import com.android.intentresolver.ui.ProfilePagerResources;
import com.android.intentresolver.ui.model.ActivityModel;
import com.android.intentresolver.ui.model.ResolverRequest;
import com.android.intentresolver.ui.viewmodel.ResolverViewModel;
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;
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.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.inject.Inject;
/**
* This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
* *not* the resolver that is actually triggered by the system right now (you want
* frameworks/base/core/java/com/android/internal/app/ResolverActivity.java for that), the full
* migration is not complete.
*/
@AndroidEntryPoint(FragmentActivity.class)
public class ResolverActivity extends Hilt_ResolverActivity implements
ResolverListAdapter.ResolverListCommunicator {
@Inject @Background public CoroutineDispatcher mBackgroundDispatcher;
@Inject public UserInteractor mUserInteractor;
@Inject public ResolverHelper mResolverHelper;
@Inject public PackageManager mPackageManager;
@Inject public DevicePolicyResources mDevicePolicyResources;
@Inject public ProfilePagerResources mProfilePagerResources;
@Inject public IntentForwarding mIntentForwarding;
@Inject public FeatureFlags mFeatureFlags;
private ResolverViewModel mViewModel;
private ResolverRequest mRequest;
private ProfileHelper mProfiles;
private ProfileAvailability mProfileAvailability;
protected TargetDataLoader mTargetDataLoader;
private boolean mResolvingHome;
private Button mAlwaysButton;
private Button mOnceButton;
protected View mProfileView;
private int mLastSelected = AbsListView.INVALID_POSITION;
private int mLayoutId;
private PickTargetOptionRequest mPickOptionRequest;
// Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
protected ResolverDrawerLayout mResolverDrawerLayout;
private static final String TAG = "ResolverActivity";
private static final boolean DEBUG = false;
private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
private boolean mRegistered;
protected Insets mSystemWindowInsets = null;
private Space mFooterSpacer = null;
protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
/** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
private final boolean mWorkProfileHasBeenEnabled = false;
protected static final String TAB_TAG_PERSONAL = "personal";
protected static final String TAB_TAG_WORK = "work";
private PackageMonitor mPersonalPackageMonitor;
private PackageMonitor mWorkPackageMonitor;
protected ResolverMultiProfilePagerAdapter mMultiProfilePagerAdapter;
public static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
public static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
private UserHandle mHeaderCreatorUser;
@Nullable
private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
return new PackageMonitor() {
@Override
public void onSomePackagesChanged() {
listAdapter.handlePackagesChanged();
}
@Override
public boolean onPackageChanged(String packageName, int uid, String[] components) {
// We care about all package changes, not just the whole package itself which is
// default behavior.
return true;
}
};
}
protected ActivityModel createActivityModel() {
return ActivityModel.createFrom(this);
}
@NonNull
@Override
public CreationExtras getDefaultViewModelCreationExtras() {
return addDefaultArgs(
super.getDefaultViewModelCreationExtras(),
new Pair<>(ActivityModel.ACTIVITY_MODEL_KEY, createActivityModel()));
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate");
setTheme(R.style.Theme_DeviceDefault_Resolver);
mResolverHelper.setInitializer(this::initialize);
}
@Override
protected final void onStart() {
super.onStart();
this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
}
@Override
protected void onStop() {
super.onStop();
final Window window = this.getWindow();
final WindowManager.LayoutParams attrs = window.getAttributes();
attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
window.setAttributes(attrs);
if (mRegistered) {
mPersonalPackageMonitor.unregister();
if (mWorkPackageMonitor != null) {
mWorkPackageMonitor.unregister();
}
mRegistered = false;
}
final Intent intent = getIntent();
if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
&& !mResolvingHome) {
// This resolver is in the unusual situation where it has been
// launched at the top of a new task. We don't let it be added
// to the recent tasks shown to the user, and we need to make sure
// that each time we are launched we get the correct launching
// uid (not re-using the same resolver from an old launching uid),
// so we will now finish ourself since being no longer visible,
// the user probably can't get back to us.
if (!isChangingConfigurations()) {
finish();
}
}
}
@Override
protected final void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager != null) {
outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
}
}
@Override
protected final void onRestart() {
super.onRestart();
if (!mRegistered) {
mPersonalPackageMonitor.register(
this,
getMainLooper(),
mProfiles.getPersonalHandle(),
false);
if (mProfiles.getWorkProfilePresent()) {
if (mWorkPackageMonitor == null) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
}
mWorkPackageMonitor.register(
this,
getMainLooper(),
mProfiles.getWorkHandle(),
false);
}
mRegistered = true;
}
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (!isChangingConfigurations() && mPickOptionRequest != null) {
mPickOptionRequest.cancel();
}
if (mMultiProfilePagerAdapter != null
&& mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
}
}
private void initialize() {
mViewModel = new ViewModelProvider(this).get(ResolverViewModel.class);
mRequest = mViewModel.getRequest().getValue();
mProfiles = new ProfileHelper(
mUserInteractor,
getCoroutineScope(getLifecycle()),
mBackgroundDispatcher,
mFeatureFlags);
mProfileAvailability = new ProfileAvailability(
mUserInteractor,
getCoroutineScope(getLifecycle()),
mBackgroundDispatcher);
mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated);
mResolvingHome = mRequest.isResolvingHome();
mTargetDataLoader = new DefaultTargetDataLoader(
this,
getLifecycle(),
mRequest.isAudioCaptureDevice());
// The last argument of createResolverListAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
// 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 multiple tabs are 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 = !isVoiceInteraction()
&& !mProfiles.getWorkProfilePresent() && !mProfiles.getCloneUserPresent();
mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
new Intent[0],
/* resolutionList = */ mRequest.getResolutionList(),
filterLastUsed
);
if (configureContentView(mTargetDataLoader)) {
return;
}
mPersonalPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getPersonalListAdapter());
mPersonalPackageMonitor.register(
this,
getMainLooper(),
mProfiles.getPersonalHandle(),
false
);
if (mProfiles.getWorkProfilePresent()) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
mWorkPackageMonitor.register(
this,
getMainLooper(),
mProfiles.getWorkHandle(),
false
);
}
mRegistered = true;
final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
if (rdl != null) {
rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
@Override
public void onDismissed() {
finish();
}
});
boolean hasTouchScreen = mPackageManager
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
if (isVoiceInteraction() || !hasTouchScreen) {
rdl.setCollapsed(false);
}
rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
mResolverDrawerLayout = rdl;
}
Intent intent = mViewModel.getRequest().getValue().getIntent();
final Set
true
if the activity is finishing and creation should halt.
*/
protected boolean postRebuildList(boolean rebuildCompleted) {
return postRebuildListInternal(rebuildCompleted);
}
/**
* Callback called when user changes the profile tab.
*/
/* TODO: consider merging with the customized considerations of our implemented
* {@link MultiProfilePagerAdapter.OnProfileSelectedListener}. The only apparent distinctions
* between the respective listener callbacks would occur in the triggering patterns during init
* (when the `OnProfileSelectedListener` is registered after a possible tab-change), or possibly
* if there's some way to trigger an update in one model but not the other. If there's an
* initialization dependency, we can probably reason about it with confidence. If there's a
* discrepancy between the `TabHost` and pager-adapter data models, that inconsistency is
* likely to be a bug that would benefit from consolidation.
*/
protected void onProfileTabSelected(int currentPage) {
setupViewVisibilities();
maybeLogProfileChange();
if (mProfiles.getWorkProfilePresent()) {
// The device policy logger is only concerned with sessions that include a work profile.
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
.setInt(currentPage)
.setStrings(getMetricsCategory())
.write();
}
}
/**
* 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);
}
}
protected void resetButtonBar() {
final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
if (buttonLayout == null) {
Log.e(TAG, "Layout unexpectedly does not have a button bar");
return;
}
ResolverListAdapter activeListAdapter =
mMultiProfilePagerAdapter.getActiveListAdapter();
View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
if (!useLayoutWithDefault()) {
int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
R.dimen.resolver_button_bar_spacing) + inset);
}
if (activeListAdapter.isTabLoaded()
&& mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
&& !useLayoutWithDefault()) {
buttonLayout.setVisibility(View.INVISIBLE);
if (buttonBarDivider != null) {
buttonBarDivider.setVisibility(View.INVISIBLE);
}
setButtonBarIgnoreOffset(/* ignoreOffset */ false);
return;
}
if (buttonBarDivider != null) {
buttonBarDivider.setVisibility(View.VISIBLE);
}
buttonLayout.setVisibility(View.VISIBLE);
setButtonBarIgnoreOffset(/* ignoreOffset */ true);
mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
resetAlwaysOrOnceButtonBar();
}
protected String getMetricsCategory() {
return METRICS_CATEGORY_RESOLVER;
}
@Override // ResolverListCommunicator
public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
if (!mMultiProfilePagerAdapter.onHandlePackagesChanged(
listAdapter,
mProfileAvailability.getWaitingToEnableProfile())) {
// We no longer have any items... just finish the activity.
finish();
}
}
protected void maybeLogProfileChange() {}
@VisibleForTesting
protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
return new CrossProfileIntentsChecker(getContentResolver());
}
private void onWorkProfileStatusUpdated() {
if (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_WORK) {
mMultiProfilePagerAdapter.rebuildActiveTab(true);
} else {
mMultiProfilePagerAdapter.clearInactiveProfileCache();
}
}
// @NonFinalForTesting
@VisibleForTesting
protected ResolverListAdapter createResolverListAdapter(
Context context,
Listtrue
if the activity is finishing and creation should halt.
*/
private boolean configureContentView(TargetDataLoader targetDataLoader) {
if (mMultiProfilePagerAdapter.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.
// To date, we really only care about "partially rebuilding" tabs for work and/or personal.
boolean rebuildCompleted =
mMultiProfilePagerAdapter.rebuildTabs(mProfiles.getWorkProfilePresent());
if (shouldUseMiniResolver()) {
configureMiniResolverContent(targetDataLoader);
Trace.endSection();
return false;
}
if (useLayoutWithDefault()) {
mLayoutId = R.layout.resolver_list_with_default;
} else {
mLayoutId = getLayoutResource();
}
setContentView(mLayoutId);
mMultiProfilePagerAdapter.setupViewPager(
findViewById(com.android.internal.R.id.profile_pager));
boolean result = postRebuildList(rebuildCompleted);
Trace.endSection();
return result;
}
/**
* Mini resolver is shown when the user is choosing between browser[s] in this profile and a
* single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
* and asks the user if they'd like to open that cross-profile app or use the in-profile
* browser.
*/
private void configureMiniResolverContent(TargetDataLoader targetDataLoader) {
mLayoutId = R.layout.miniresolver;
setContentView(mLayoutId);
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
ResolverListAdapter sameProfileAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getPersonalListAdapter()
: mMultiProfilePagerAdapter.getWorkListAdapter();
ResolverListAdapter inactiveAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getWorkListAdapter()
: mMultiProfilePagerAdapter.getPersonalListAdapter();
DisplayResolveInfo sameProfileResolveInfo = sameProfileAdapter.getFirstDisplayResolveInfo();
final DisplayResolveInfo otherProfileResolveInfo =
inactiveAdapter.getFirstDisplayResolveInfo();
// Load the icon asynchronously
ImageView icon = findViewById(com.android.internal.R.id.icon);
targetDataLoader.getOrLoadAppTargetIcon(
otherProfileResolveInfo,
inactiveAdapter.getUserHandle(),
(drawable) -> {
if (!isDestroyed()) {
otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
}
});
((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
getResources().getString(
inWorkProfile
? R.string.miniresolver_open_in_personal
: R.string.miniresolver_open_in_work,
getOrLoadDisplayLabel(otherProfileResolveInfo)));
((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
inWorkProfile ? R.string.miniresolver_use_work_browser
: R.string.miniresolver_use_personal_browser);
findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener(
v -> {
safelyStartActivity(sameProfileResolveInfo);
finish();
});
findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
Intent intent = otherProfileResolveInfo.getResolvedIntent();
safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle());
finish();
});
}
private boolean isTwoPagePersonalAndWorkConfiguration() {
return (mMultiProfilePagerAdapter.getCount() == 2)
&& mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL)
&& mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);
}
@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
if (!cti.isSuspended() && mRegistered) {
if (mPersonalPackageMonitor != null) {
mPersonalPackageMonitor.unregister();
}
if (mWorkPackageMonitor != null) {
mWorkPackageMonitor.unregister();
}
mRegistered = false;
}
// If needed, show that intent is forwarded
// from managed profile to owner or other way around.
String profileSwitchMessage =
mIntentForwarding.forwardMessageFor(mRequest.getIntent());
if (profileSwitchMessage != null) {
Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show();
}
try {
if (cti.startAsCaller(this, options, user.getIdentifier())) {
maybeLogCrossProfileTargetLaunch(cti, user);
}
} catch (RuntimeException e) {
Slog.wtf(TAG,
"Unable to launch as uid "
+ mViewModel.getActivityModel().getLaunchedFromUid()
+ " package " + mViewModel.getActivityModel().getLaunchedFromPackage()
+ ", while running in " + ActivityThread.currentProcessName(), e);
}
}
/**
* 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 = mMultiProfilePagerAdapter.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()) {
setupProfileTabs();
}
return false;
}
/**
* Mini resolver should be used when all of the following are true:
* 1. This is the intent picker (ResolverActivity).
* 2. This profile only has web browser matches.
* 3. The other profile has a single non-browser match.
*/
private boolean shouldUseMiniResolver() {
if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
ResolverListAdapter sameProfileAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getPersonalListAdapter()
: mMultiProfilePagerAdapter.getWorkListAdapter();
ResolverListAdapter otherProfileAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getWorkListAdapter()
: mMultiProfilePagerAdapter.getPersonalListAdapter();
if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
Log.d(TAG, "No targets in the current profile");
return false;
}
if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
return false;
}
if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
Log.d(TAG, "Other profile is a web browser");
return false;
}
if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
Log.d(TAG, "Non-browser found in this profile");
return false;
}
return true;
}
private boolean maybeAutolaunchIfSingleTarget() {
int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
if (count != 1) {
return false;
}
if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
return false;
}
// Only one target, so we're a candidate to auto-launch!
final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
.targetInfoForPosition(0, false);
if (shouldAutoLaunchSingleChoice(target)) {
safelyStartActivity(target);
finish();
return true;
}
return false;
}
/**
* When we have just a personal and a work profile, we auto launch in the following scenario:
* - There is 1 resolved target on each profile
* - That target is the same app on both profiles
* - The target app has permission to communicate cross profiles
* - The target app has declared it supports cross-profile communication via manifest metadata
*/
private boolean maybeAutolaunchIfCrossProfileSupported() {
if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
ResolverListAdapter activeListAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getPersonalListAdapter()
: mMultiProfilePagerAdapter.getWorkListAdapter();
ResolverListAdapter inactiveListAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getWorkListAdapter()
: mMultiProfilePagerAdapter.getPersonalListAdapter();
if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
return false;
}
if ((activeListAdapter.getUnfilteredCount() != 1)
|| (inactiveListAdapter.getUnfilteredCount() != 1)) {
return false;
}
TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false);
TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
if (!Objects.equals(
activeProfileTarget.getResolvedComponentName(),
inactiveProfileTarget.getResolvedComponentName())) {
return false;
}
if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
return false;
}
String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {
return false;
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
.setBoolean(activeListAdapter.getUserHandle()
.equals(mProfiles.getPersonalHandle()))
.setStrings(getMetricsCategory())
.write();
safelyStartActivity(activeProfileTarget);
finish();
return true;
}
private boolean isAutolaunching() {
return !mRegistered && isFinishing();
}
/**
* @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
*/
private boolean maybeAutolaunchActivity() {
if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
ResolverListAdapter activeListAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getPersonalListAdapter()
: mMultiProfilePagerAdapter.getWorkListAdapter();
ResolverListAdapter inactiveListAdapter =
(mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
? mMultiProfilePagerAdapter.getWorkListAdapter()
: mMultiProfilePagerAdapter.getPersonalListAdapter();
if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
return false;
}
if ((activeListAdapter.getUnfilteredCount() != 1)
|| (inactiveListAdapter.getUnfilteredCount() != 1)) {
return false;
}
TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false);
TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
if (!Objects.equals(
activeProfileTarget.getResolvedComponentName(),
inactiveProfileTarget.getResolvedComponentName())) {
return false;
}
if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
return false;
}
String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {
return false;
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
.setBoolean(activeListAdapter.getUserHandle()
.equals(mProfiles.getPersonalHandle()))
.setStrings(getMetricsCategory())
.write();
safelyStartActivity(activeProfileTarget);
finish();
return true;
}
private void maybeHideDivider() {
final View divider = findViewById(com.android.internal.R.id.divider);
if (divider == null) {
return;
}
divider.setVisibility(View.GONE);
}
private void resetCheckedItem() {
mLastSelected = ListView.INVALID_POSITION;
((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
.clearCheckedItemsInInactiveProfiles();
}
private void setupViewVisibilities() {
ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
addUseDifferentAppLabelIfNecessary(activeListAdapter);
}
}
/**
* Updates the button bar container {@code ignoreOffset} layout param.
* Setting this to {@code true} means that the button bar will be glued to the bottom of
* the screen.
*/
private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
if (buttonBarContainer != null) {
ResolverDrawerLayout.LayoutParams layoutParams =
(ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
layoutParams.ignoreOffset = ignoreOffset;
buttonBarContainer.setLayoutParams(layoutParams);
}
}
private void setupAdapterListView(ListView listView, ItemClickListener listener) {
listView.setOnItemClickListener(listener);
listView.setOnItemLongClickListener(listener);
listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
}
/**
* Configure the area above the app selection list (title, content preview, etc).
*/
private void maybeCreateHeader(ResolverListAdapter listAdapter) {
if (mHeaderCreatorUser != null
&& !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
return;
}
if (!mProfiles.getWorkProfilePresent()
&& listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
final TextView titleView = findViewById(com.android.internal.R.id.title);
if (titleView != null) {
titleView.setVisibility(View.GONE);
}
}
ResolverRequest request = mViewModel.getRequest().getValue();
CharSequence title = mViewModel.getRequest().getValue().getTitle() != null
? request.getTitle()
: getTitleForAction(request.getIntent(), 0);
if (!TextUtils.isEmpty(title)) {
final TextView titleView = findViewById(com.android.internal.R.id.title);
if (titleView != null) {
titleView.setText(title);
}
setTitle(title);
}
final ImageView iconView = findViewById(com.android.internal.R.id.icon);
if (iconView != null) {
listAdapter.loadFilteredItemIconTaskAsync(iconView);
}
mHeaderCreatorUser = listAdapter.getUserHandle();
}
private void resetAlwaysOrOnceButtonBar() {
// Disable both buttons initially
setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false);
mOnceButton.setEnabled(false);
int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()
.getFilteredPosition();
if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) {
setAlwaysButtonEnabled(true, filteredPosition, false);
mOnceButton.setEnabled(true);
// Focus the button if we already have the default option
mOnceButton.requestFocus();
return;
}
// When the items load in, if an item was already selected, enable the buttons
ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
if (currentAdapterView != null
&& currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true);
mOnceButton.setEnabled(true);
}
}
@Override // ResolverListCommunicator
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.
return mMultiProfilePagerAdapter.getListAdapterForUserHandle(
mProfiles.getTabOwnerUserHandleForLaunch()
).hasFilteredItem();
}
final class ItemClickListener implements AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
final ListView listView = parent instanceof ListView ? (ListView) parent : null;
if (listView != null) {
position -= listView.getHeaderViewsCount();
}
if (position < 0) {
// Header views don't count.
return;
}
// If we're still loading, we can't yet enable the buttons.
if (mMultiProfilePagerAdapter.getActiveListAdapter()
.resolveInfoForPosition(position, true) == null) {
return;
}
ListView currentAdapterView =
(ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
final int checkedPos = currentAdapterView.getCheckedItemPosition();
final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
if (!useLayoutWithDefault()
&& (!hasValidSelection || mLastSelected != checkedPos)
&& mAlwaysButton != null) {
setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
mOnceButton.setEnabled(hasValidSelection);
if (hasValidSelection) {
currentAdapterView.smoothScrollToPosition(checkedPos);
mOnceButton.requestFocus();
}
mLastSelected = checkedPos;
} else {
startSelected(position, false, true);
}
}
@Override
public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
final ListView listView = parent instanceof ListView ? (ListView) parent : null;
if (listView != null) {
position -= listView.getHeaderViewsCount();
}
if (position < 0) {
// Header views don't count.
return false;
}
ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
.resolveInfoForPosition(position, true);
showTargetDetails(ri);
return true;
}
}
private void setupProfileTabs() {
maybeHideDivider();
TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
mMultiProfilePagerAdapter.setupProfileTabs(
getLayoutInflater(),
tabHost,
viewPager,
R.layout.resolver_profile_tab_button,
com.android.internal.R.id.profile_pager,
() -> onProfileTabSelected(viewPager.getCurrentItem()),
new OnProfileSelectedListener() {
@Override
public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {
resetButtonBar();
resetCheckedItem();
}
@Override
public void onProfilePageStateChanged(int state) {}
});
mOnSwitchOnWorkSelectedListener = () -> {
final View workTab =
tabHost.getTabWidget().getChildAt(
mMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK));
workTab.setFocusable(true);
workTab.setFocusableInTouchMode(true);
workTab.requestFocus();
};
}
static final class PickTargetOptionRequest extends PickOptionRequest {
public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
@Nullable Bundle extras) {
super(prompt, options, extras);
}
@Override
public void onCancel() {
super.onCancel();
final ResolverActivity ra = (ResolverActivity) getActivity();
if (ra != null) {
ra.mPickOptionRequest = null;
ra.finish();
}
}
@Override
public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
super.onPickOptionResult(finished, selections, result);
if (selections.length != 1) {
// TODO In a better world we would filter the UI presented here and let the
// user refine. Maybe later.
return;
}
final ResolverActivity ra = (ResolverActivity) getActivity();
if (ra != null) {
final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()
.getItem(selections[0].getIndex());
if (ra.onTargetSelected(ti, false)) {
ra.mPickOptionRequest = null;
ra.finish();
}
}
}
}
/**
* 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 mProfiles.getQueryIntentsHandle(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