/* * Copyright (C) 2014 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.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL; import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK; import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY; import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_CALLING_USER; import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_SELECTED_PROFILE; import android.app.Activity; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.admin.DevicePolicyManager; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.UserInfo; import android.metrics.LogMaker; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; import android.provider.Settings; import android.util.Slog; import android.widget.Toast; import androidx.annotation.Nullable; import com.android.intentresolver.profiles.MultiProfilePagerAdapter; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * This is used in conjunction with * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to * be passed in and out of a managed profile. */ public class IntentForwarderActivity extends Activity { public static String TAG = "IntentForwarderActivity"; public static String FORWARD_INTENT_TO_PARENT = "com.android.internal.app.ForwardIntentToParent"; public static String FORWARD_INTENT_TO_MANAGED_PROFILE = "com.android.internal.app.ForwardIntentToManagedProfile"; private static final Set ALLOWED_TEXT_MESSAGE_SCHEMES = new HashSet<>(Arrays.asList("sms", "smsto", "mms", "mmsto")); private static final String TEL_SCHEME = "tel"; private static final ComponentName RESOLVER_COMPONENT_NAME = new ComponentName("android", ResolverActivity.class.getName()); private Injector mInjector; private MetricsLogger mMetricsLogger; protected ExecutorService mExecutorService; @Override protected void onDestroy() { super.onDestroy(); mExecutorService.shutdown(); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mInjector = createInjector(); mExecutorService = Executors.newSingleThreadExecutor(); Intent intentReceived = getIntent(); String className = intentReceived.getComponent().getClassName(); final int targetUserId; final String userMessage; if (className.equals(FORWARD_INTENT_TO_PARENT)) { userMessage = getForwardToPersonalMessage(); targetUserId = getProfileParent(); getMetricsLogger().write( new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE) .setSubtype(MetricsEvent.PARENT_PROFILE)); } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { userMessage = getForwardToWorkMessage(); targetUserId = getManagedProfile(); getMetricsLogger().write( new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE) .setSubtype(MetricsEvent.MANAGED_PROFILE)); } else { Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly"); userMessage = null; targetUserId = UserHandle.USER_NULL; } if (targetUserId == UserHandle.USER_NULL) { // This covers the case where there is no parent / managed profile. finish(); return; } if (Intent.ACTION_CHOOSER.equals(intentReceived.getAction())) { launchChooserActivityWithCorrectTab(intentReceived, className); return; } final int callingUserId = getUserId(); final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId, mInjector.getIPackageManager(), getContentResolver()); if (newIntent == null) { Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user " + callingUserId + " to user " + targetUserId); finish(); return; } newIntent.prepareToLeaveUser(callingUserId); final CompletableFuture targetResolveInfoFuture = mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId); targetResolveInfoFuture .thenApplyAsync(targetResolveInfo -> { if (isResolverActivityResolveInfo(targetResolveInfo)) { launchResolverActivityWithCorrectTab(intentReceived, className, newIntent, callingUserId, targetUserId); return targetResolveInfo; } startActivityAsCaller(newIntent, targetUserId); return targetResolveInfo; }, mExecutorService) .thenAcceptAsync(result -> { maybeShowDisclosure(intentReceived, result, userMessage); finish(); }, getApplicationContext().getMainExecutor()); } private String getForwardToPersonalMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_PERSONAL, () -> getString(R.string.forward_intent_to_owner)); } private String getForwardToWorkMessage() { return getSystemService(DevicePolicyManager.class).getResources().getString( FORWARD_INTENT_TO_WORK, () -> getString(R.string.forward_intent_to_work)); } private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) { if (resolveInfo == null) { return false; } ActivityInfo activityInfo = resolveInfo.activityInfo; if (activityInfo == null) { return false; } if (!"android".equals(activityInfo.packageName)) { return false; } return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT) || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE); } private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) { return resolveInfo != null && resolveInfo.activityInfo != null && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName()); } private void maybeShowDisclosure( Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) { if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) { mInjector.showToast(message, Toast.LENGTH_LONG); } } private void startActivityAsCaller(Intent newIntent, int userId) { try { startActivityAsCaller( newIntent, /* options= */ null, /* ignoreTargetSecurity= */ false, userId); } catch (RuntimeException e) { Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package " + getLaunchedFromPackage() + ", while running in " + ActivityThread.currentProcessName(), e); } } private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) { // When showing the sharesheet, instead of forwarding to the other profile, // we launch the sharesheet in the current user and select the other tab. // This fixes b/152866292 where the user can not go back to the original profile // when cross-profile intents are disabled. int selectedProfile = findSelectedProfile(className); sanitizeIntent(intentReceived); intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile); Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT); if (innerIntent == null) { Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT); return; } sanitizeIntent(innerIntent); startActivityAsCaller(intentReceived, null, false, getUserId()); finish(); } private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className, Intent newIntent, int callingUserId, int targetUserId) { // When showing the intent resolver, instead of forwarding to the other profile, // we launch it in the current user and select the other tab. This fixes b/155874820. // // In the case when there are 0 targets in the current profile and >1 apps in the other // profile, the package manager launches the intent resolver in the other profile. // If that's the case, we launch the resolver in the target user instead (other profile). ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser( newIntent, MATCH_DEFAULT_ONLY, callingUserId).join(); int userId = isIntentForwarderResolveInfo(callingResolveInfo) ? targetUserId : callingUserId; int selectedProfile = findSelectedProfile(className); sanitizeIntent(intentReceived); intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile); intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId)); startActivityAsCaller(intentReceived, null, false, userId); finish(); } private int findSelectedProfile(String className) { if (className.equals(FORWARD_INTENT_TO_PARENT)) { return MultiProfilePagerAdapter.PROFILE_PERSONAL; } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) { return MultiProfilePagerAdapter.PROFILE_WORK; } return -1; } private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) { if (!isDeviceProvisioned()) { return false; } if (ri == null || ri.activityInfo == null) { return true; } if (ri.activityInfo.applicationInfo.isSystemApp() && (isDialerIntent(intent) || isTextMessageIntent(intent))) { return false; } return !isTargetResolverOrChooserActivity(ri.activityInfo); } private boolean isDeviceProvisioned() { return Settings.Global.getInt(getContentResolver(), Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0; } private boolean isTextMessageIntent(Intent intent) { return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent)) && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme()); } private boolean isDialerIntent(Intent intent) { return Intent.ACTION_DIAL.equals(intent.getAction()) || Intent.ACTION_CALL.equals(intent.getAction()) || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction()) || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction()) || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme())); } private boolean isViewActionIntent(Intent intent) { return Intent.ACTION_VIEW.equals(intent.getAction()) && intent.hasCategory(Intent.CATEGORY_BROWSABLE); } private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) { if (!"android".equals(activityInfo.packageName)) { return false; } return ResolverActivity.class.getName().equals(activityInfo.name) || ChooserActivity.class.getName().equals(activityInfo.name); } /** * Check whether the intent can be forwarded to target user. Return the intent used for * forwarding if it can be forwarded, {@code null} otherwise. */ public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId, IPackageManager packageManager, ContentResolver contentResolver) { Intent forwardIntent = new Intent(incomingIntent); forwardIntent.addFlags( Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP); sanitizeIntent(forwardIntent); Intent intentToCheck = forwardIntent; if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) { return null; } if (forwardIntent.getSelector() != null) { intentToCheck = forwardIntent.getSelector(); } String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver); sanitizeIntent(intentToCheck); try { if (packageManager.canForwardTo( intentToCheck, resolvedType, sourceUserId, targetUserId)) { return forwardIntent; } } catch (RemoteException e) { Slog.e(TAG, "PackageManagerService is dead?"); } return null; } /** * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is * no managed profile. * * TODO: Remove the assumption that there is only one managed profile * on the device. */ private int getManagedProfile() { List relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId()); for (UserInfo userInfo : relatedUsers) { if (userInfo.isManagedProfile()) return userInfo.id; } Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE + " has been called, but there is no managed profile"); return UserHandle.USER_NULL; } /** * Returns the userId of the profile parent or UserHandle.USER_NULL if there is * no parent. */ private int getProfileParent() { UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId()); if (parent == null) { Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT + " has been called, but there is no parent"); return UserHandle.USER_NULL; } return parent.id; } /** * Sanitize the intent in place. */ private static void sanitizeIntent(Intent intent) { // Apps should not be allowed to target a specific package/ component in the target user. intent.setPackage(null); intent.setComponent(null); } protected MetricsLogger getMetricsLogger() { if (mMetricsLogger == null) { mMetricsLogger = new MetricsLogger(); } return mMetricsLogger; } @VisibleForTesting protected Injector createInjector() { return new InjectorImpl(); } private class InjectorImpl implements Injector { @Override public IPackageManager getIPackageManager() { return AppGlobals.getPackageManager(); } @Override public UserManager getUserManager() { return getSystemService(UserManager.class); } @Override public PackageManager getPackageManager() { return IntentForwarderActivity.this.getPackageManager(); } @Override @Nullable public CompletableFuture resolveActivityAsUser( Intent intent, int flags, int userId) { return CompletableFuture.supplyAsync( () -> getPackageManager().resolveActivityAsUser(intent, flags, userId)); } @Override public void showToast(String message, int duration) { Toast.makeText(IntentForwarderActivity.this, message, duration).show(); } } public interface Injector { IPackageManager getIPackageManager(); UserManager getUserManager(); PackageManager getPackageManager(); CompletableFuture resolveActivityAsUser(Intent intent, int flags, int userId); void showToast(String message, int duration); } }