| /* |
| * Copyright (C) 2016 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.server.wm; |
| |
| import static android.app.ActivityManager.INTENT_SENDER_ACTIVITY; |
| import static android.app.ActivityOptions.ANIM_OPEN_CROSS_PROFILE_APPS; |
| import static android.app.PendingIntent.FLAG_CANCEL_CURRENT; |
| import static android.app.PendingIntent.FLAG_IMMUTABLE; |
| import static android.app.PendingIntent.FLAG_ONE_SHOT; |
| import static android.app.admin.DevicePolicyManager.EXTRA_RESTRICTION; |
| import static android.app.admin.DevicePolicyManager.POLICY_SUSPEND_PACKAGES; |
| import static android.content.Context.KEYGUARD_SERVICE; |
| import static android.content.Intent.EXTRA_INTENT; |
| import static android.content.Intent.EXTRA_PACKAGE_NAME; |
| import static android.content.Intent.EXTRA_TASK_ID; |
| import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS; |
| import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; |
| import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME; |
| import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED; |
| |
| import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; |
| |
| import android.annotation.Nullable; |
| import android.app.ActivityOptions; |
| import android.app.KeyguardManager; |
| import android.app.TaskInfo; |
| import android.app.admin.DevicePolicyManagerInternal; |
| import android.content.Context; |
| import android.content.IIntentSender; |
| import android.content.Intent; |
| import android.content.IntentSender; |
| import android.content.pm.ActivityInfo; |
| import android.content.pm.PackageManagerInternal; |
| import android.content.pm.ResolveInfo; |
| import android.content.pm.SuspendDialogInfo; |
| import android.content.pm.UserInfo; |
| import android.content.pm.UserPackage; |
| import android.os.Bundle; |
| import android.os.IBinder; |
| import android.os.RemoteException; |
| import android.os.UserHandle; |
| import android.os.UserManager; |
| import android.util.Pair; |
| import android.util.SparseArray; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| import com.android.internal.app.BlockedAppActivity; |
| import com.android.internal.app.HarmfulAppWarningActivity; |
| import com.android.internal.app.SuspendedAppActivity; |
| import com.android.internal.app.UnlaunchableAppActivity; |
| import com.android.server.LocalServices; |
| import com.android.server.am.ActivityManagerService; |
| import com.android.server.wm.ActivityInterceptorCallback.ActivityInterceptResult; |
| |
| /** |
| * A class that contains activity intercepting logic for {@link ActivityStarter#execute()} |
| * It's initialized via setStates and interception occurs via the intercept method. |
| * |
| * Note that this class is instantiated when {@link ActivityManagerService} gets created so there |
| * is no guarantee that other system services are already present. |
| */ |
| class ActivityStartInterceptor { |
| |
| private final ActivityTaskManagerService mService; |
| private final ActivityTaskSupervisor mSupervisor; |
| private final Context mServiceContext; |
| |
| // UserManager cannot be final as it's not ready when this class is instantiated during boot |
| private UserManager mUserManager; |
| |
| /* |
| * Per-intent states loaded from ActivityStarter than shouldn't be changed by any |
| * interception routines. |
| */ |
| private int mRealCallingPid; |
| private int mRealCallingUid; |
| private int mUserId; |
| private int mStartFlags; |
| private String mCallingPackage; |
| private @Nullable String mCallingFeatureId; |
| |
| /* |
| * Per-intent states that were load from ActivityStarter and are subject to modifications |
| * by the interception routines. After calling {@link #intercept} the caller should assign |
| * these values back to {@link ActivityStarter#startActivityLocked}'s local variables if |
| * {@link #intercept} returns true. |
| */ |
| Intent mIntent; |
| int mCallingPid; |
| int mCallingUid; |
| ResolveInfo mRInfo; |
| ActivityInfo mAInfo; |
| String mResolvedType; |
| Task mInTask; |
| TaskFragment mInTaskFragment; |
| ActivityOptions mActivityOptions; |
| |
| /* |
| * Note that this is just a hint of what the launch display area will be as it is |
| * based only on the information at the early pre-interception stage of starting the |
| * intent. The real launch display area calculated later may be different from this one. |
| */ |
| TaskDisplayArea mPresumableLaunchDisplayArea; |
| |
| ActivityStartInterceptor( |
| ActivityTaskManagerService service, ActivityTaskSupervisor supervisor) { |
| this(service, supervisor, service.mContext); |
| } |
| |
| @VisibleForTesting |
| ActivityStartInterceptor(ActivityTaskManagerService service, ActivityTaskSupervisor supervisor, |
| Context context) { |
| mService = service; |
| mSupervisor = supervisor; |
| mServiceContext = context; |
| } |
| |
| /** |
| * Effectively initialize the class before intercepting the start intent. The values set in this |
| * method should not be changed during intercept. |
| */ |
| void setStates(int userId, int realCallingPid, int realCallingUid, int startFlags, |
| String callingPackage, @Nullable String callingFeatureId) { |
| mRealCallingPid = realCallingPid; |
| mRealCallingUid = realCallingUid; |
| mUserId = userId; |
| mStartFlags = startFlags; |
| mCallingPackage = callingPackage; |
| mCallingFeatureId = callingFeatureId; |
| } |
| |
| private IntentSender createIntentSenderForOriginalIntent(int callingUid, int flags) { |
| Bundle bOptions = deferCrossProfileAppsAnimationIfNecessary(); |
| final TaskFragment taskFragment = getLaunchTaskFragment(); |
| // If the original intent is going to be embedded, try to forward the embedding TaskFragment |
| // and its task id to embed back the original intent. |
| if (taskFragment != null) { |
| ActivityOptions activityOptions = bOptions != null |
| ? ActivityOptions.fromBundle(bOptions) |
| : ActivityOptions.makeBasic(); |
| activityOptions.setLaunchTaskFragmentToken(taskFragment.getFragmentToken()); |
| bOptions = activityOptions.toBundle(); |
| } |
| final IIntentSender target = mService.getIntentSenderLocked( |
| INTENT_SENDER_ACTIVITY, mCallingPackage, mCallingFeatureId, callingUid, mUserId, |
| null /*token*/, null /*resultCode*/, 0 /*requestCode*/, |
| new Intent[] { mIntent }, new String[] { mResolvedType }, |
| flags, bOptions); |
| return new IntentSender(target); |
| } |
| |
| |
| /** |
| * A helper function to obtain the targeted {@link TaskFragment} during |
| * {@link #intercept(Intent, ResolveInfo, ActivityInfo, String, Task, TaskFragment, int, int, |
| * ActivityOptions, TaskDisplayArea)} if any. |
| */ |
| @Nullable |
| private TaskFragment getLaunchTaskFragment() { |
| if (mInTaskFragment != null) { |
| return mInTaskFragment; |
| } |
| if (mActivityOptions == null) { |
| return null; |
| } |
| final IBinder taskFragToken = mActivityOptions.getLaunchTaskFragmentToken(); |
| if (taskFragToken == null) { |
| return null; |
| } |
| return TaskFragment.fromTaskFragmentToken(taskFragToken, mService); |
| } |
| |
| /** |
| * Intercept the launch intent based on various signals. If an interception happened the |
| * internal variables get assigned and need to be read explicitly by the caller. |
| * |
| * @return true if an interception occurred |
| */ |
| boolean intercept(Intent intent, ResolveInfo rInfo, ActivityInfo aInfo, String resolvedType, |
| Task inTask, TaskFragment inTaskFragment, int callingPid, int callingUid, |
| ActivityOptions activityOptions, TaskDisplayArea presumableLaunchDisplayArea) { |
| mUserManager = UserManager.get(mServiceContext); |
| |
| mIntent = intent; |
| mCallingPid = callingPid; |
| mCallingUid = callingUid; |
| mRInfo = rInfo; |
| mAInfo = aInfo; |
| mResolvedType = resolvedType; |
| mInTask = inTask; |
| mInTaskFragment = inTaskFragment; |
| mActivityOptions = activityOptions; |
| mPresumableLaunchDisplayArea = presumableLaunchDisplayArea; |
| |
| if (interceptQuietProfileIfNeeded()) { |
| // If work profile is turned off, skip the work challenge since the profile can only |
| // be unlocked when profile's user is running. |
| return true; |
| } |
| if (interceptSuspendedPackageIfNeeded()) { |
| // Skip the rest of interceptions as the package is suspended by device admin so |
| // no user action can undo this. |
| return true; |
| } |
| if (interceptLockTaskModeViolationPackageIfNeeded()) { |
| return true; |
| } |
| if (interceptHarmfulAppIfNeeded()) { |
| // If the app has a "harmful app" warning associated with it, we should ask to uninstall |
| // before issuing the work challenge. |
| return true; |
| } |
| if (interceptLockedManagedProfileIfNeeded()) { |
| return true; |
| } |
| if (interceptHomeIfNeeded()) { |
| // Replace primary home intents directed at displays that do not support primary home |
| // but support secondary home with the relevant secondary home activity. |
| return true; |
| } |
| |
| final SparseArray<ActivityInterceptorCallback> callbacks = |
| mService.getActivityInterceptorCallbacks(); |
| final ActivityInterceptorCallback.ActivityInterceptorInfo interceptorInfo = |
| getInterceptorInfo(null /* clearOptionsAnimation */); |
| |
| for (int i = 0; i < callbacks.size(); i++) { |
| final ActivityInterceptorCallback callback = callbacks.valueAt(i); |
| final ActivityInterceptResult interceptResult = callback.onInterceptActivityLaunch( |
| interceptorInfo); |
| if (interceptResult == null) { |
| continue; |
| } |
| mIntent = interceptResult.getIntent(); |
| mActivityOptions = interceptResult.getActivityOptions(); |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| if (interceptResult.isActivityResolved()) { |
| return true; |
| } |
| mRInfo = mSupervisor.resolveIntent(mIntent, null, mUserId, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, |
| null /*profilerInfo*/); |
| return true; |
| } |
| return false; |
| } |
| |
| private boolean hasCrossProfileAnimation() { |
| return mActivityOptions != null |
| && mActivityOptions.getAnimationType() == ANIM_OPEN_CROSS_PROFILE_APPS; |
| } |
| |
| /** |
| * If the activity option is the {@link ActivityOptions#ANIM_OPEN_CROSS_PROFILE_APPS} one, |
| * defer the animation until the original intent is started. |
| * |
| * @return the activity option used to start the original intent. |
| */ |
| private Bundle deferCrossProfileAppsAnimationIfNecessary() { |
| if (hasCrossProfileAnimation()) { |
| mActivityOptions = null; |
| return ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(); |
| } |
| return null; |
| } |
| |
| private boolean interceptQuietProfileIfNeeded() { |
| // Do not intercept if the user has not turned off the profile |
| if (!mUserManager.isQuietModeEnabled(UserHandle.of(mUserId))) { |
| return false; |
| } |
| |
| IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, |
| FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT); |
| |
| mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target, mRInfo); |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| |
| final UserInfo parent = mUserManager.getProfileParent(mUserId); |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, parent.id, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| private boolean interceptSuspendedByAdminPackage() { |
| DevicePolicyManagerInternal devicePolicyManager = LocalServices |
| .getService(DevicePolicyManagerInternal.class); |
| if (devicePolicyManager == null) { |
| return false; |
| } |
| mIntent = devicePolicyManager.createShowAdminSupportIntent(mUserId, true); |
| mIntent.putExtra(EXTRA_RESTRICTION, POLICY_SUSPEND_PACKAGES); |
| |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| |
| final UserInfo parent = mUserManager.getProfileParent(mUserId); |
| if (parent != null) { |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, parent.id, 0, |
| mRealCallingUid, mRealCallingPid); |
| } else { |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, 0, |
| mRealCallingUid, mRealCallingPid); |
| } |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| private boolean interceptSuspendedPackageIfNeeded() { |
| // Do not intercept if the package is not suspended |
| if (!isPackageSuspended()) { |
| return false; |
| } |
| final PackageManagerInternal pmi = mService.getPackageManagerInternalLocked(); |
| if (pmi == null) { |
| return false; |
| } |
| final String suspendedPackage = mAInfo.applicationInfo.packageName; |
| final UserPackage suspender = pmi.getSuspendingPackage(suspendedPackage, mUserId); |
| if (suspender != null && PLATFORM_PACKAGE_NAME.equals(suspender.packageName)) { |
| return interceptSuspendedByAdminPackage(); |
| } |
| final SuspendDialogInfo dialogInfo = pmi.getSuspendedDialogInfo(suspendedPackage, |
| suspender, mUserId); |
| final Bundle crossProfileOptions = hasCrossProfileAnimation() |
| ? ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle() |
| : null; |
| final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, |
| FLAG_IMMUTABLE); |
| mIntent = SuspendedAppActivity.createSuspendedAppInterceptIntent(suspendedPackage, |
| suspender, dialogInfo, crossProfileOptions, target, mUserId); |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| private boolean interceptLockTaskModeViolationPackageIfNeeded() { |
| if (mAInfo == null || mAInfo.applicationInfo == null) { |
| return false; |
| } |
| LockTaskController controller = mService.getLockTaskController(); |
| String packageName = mAInfo.applicationInfo.packageName; |
| int lockTaskLaunchMode = ActivityRecord.getLockTaskLaunchMode(mAInfo, mActivityOptions); |
| if (controller.isActivityAllowed(mUserId, packageName, lockTaskLaunchMode)) { |
| return false; |
| } |
| mIntent = BlockedAppActivity.createIntent(mUserId, mAInfo.applicationInfo.packageName); |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| private boolean interceptLockedManagedProfileIfNeeded() { |
| final Intent interceptingIntent = interceptWithConfirmCredentialsIfNeeded(mAInfo, mUserId); |
| if (interceptingIntent == null) { |
| return false; |
| } |
| mIntent = interceptingIntent; |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| final TaskFragment taskFragment = getLaunchTaskFragment(); |
| // If we are intercepting and there was a task, convert it into an extra for the |
| // ConfirmCredentials intent and unassign it, as otherwise the task will move to |
| // front even if ConfirmCredentials is cancelled. |
| if (mInTask != null) { |
| mIntent.putExtra(EXTRA_TASK_ID, mInTask.mTaskId); |
| mInTask = null; |
| } else if (taskFragment != null) { |
| // If the original intent is started to an embedded TaskFragment, append its parent task |
| // id to extra. It is to embed back the original intent to the TaskFragment with the |
| // same task. |
| final Task parentTask = taskFragment.getTask(); |
| if (parentTask != null) { |
| mIntent.putExtra(EXTRA_TASK_ID, parentTask.mTaskId); |
| } |
| } |
| if (mActivityOptions == null) { |
| mActivityOptions = ActivityOptions.makeBasic(); |
| } |
| |
| final UserInfo parent = mUserManager.getProfileParent(mUserId); |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, parent.id, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| /** |
| * Creates an intent to intercept the current activity start with Confirm Credentials if needed. |
| * |
| * @return The intercepting intent if needed. |
| */ |
| private Intent interceptWithConfirmCredentialsIfNeeded(ActivityInfo aInfo, int userId) { |
| if (!mService.mAmInternal.shouldConfirmCredentials(userId)) { |
| return null; |
| } |
| if ((aInfo.flags & ActivityInfo.FLAG_SHOW_WHEN_LOCKED) != 0 |
| && (mUserManager.isUserUnlocked(userId) || aInfo.directBootAware)) { |
| return null; |
| } |
| final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, |
| FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE); |
| final KeyguardManager km = (KeyguardManager) mServiceContext |
| .getSystemService(KEYGUARD_SERVICE); |
| final Intent newIntent = km.createConfirmDeviceCredentialIntent(null, null, userId, |
| true /* disallowBiometricsIfPolicyExists */); |
| if (newIntent == null) { |
| return null; |
| } |
| newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS | |
| FLAG_ACTIVITY_TASK_ON_HOME); |
| newIntent.putExtra(EXTRA_PACKAGE_NAME, aInfo.packageName); |
| newIntent.putExtra(EXTRA_INTENT, target); |
| return newIntent; |
| } |
| |
| private boolean interceptHarmfulAppIfNeeded() { |
| CharSequence harmfulAppWarning; |
| try { |
| harmfulAppWarning = mService.getPackageManager() |
| .getHarmfulAppWarning(mAInfo.packageName, mUserId); |
| } catch (RemoteException | IllegalArgumentException ex) { |
| return false; |
| } |
| |
| if (harmfulAppWarning == null) { |
| return false; |
| } |
| |
| final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid, |
| FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE); |
| |
| mIntent = HarmfulAppWarningActivity.createHarmfulAppWarningIntent(mServiceContext, |
| mAInfo.packageName, target, harmfulAppWarning); |
| |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/); |
| return true; |
| } |
| |
| private boolean interceptHomeIfNeeded() { |
| if (mPresumableLaunchDisplayArea == null || mService.mRootWindowContainer == null) { |
| return false; |
| } |
| if (!ActivityRecord.isHomeIntent(mIntent)) { |
| return false; |
| } |
| if (!mIntent.hasCategory(Intent.CATEGORY_HOME)) { |
| // Already a secondary home intent, leave it alone. |
| return false; |
| } |
| if (mService.mRootWindowContainer.shouldPlacePrimaryHomeOnDisplay( |
| mPresumableLaunchDisplayArea.getDisplayId())) { |
| // Primary home can be launched to the display area. |
| return false; |
| } |
| if (!mService.mRootWindowContainer.shouldPlaceSecondaryHomeOnDisplayArea( |
| mPresumableLaunchDisplayArea)) { |
| // Secondary home cannot be launched on the display area. |
| return false; |
| } |
| |
| // At this point we have a primary home intent for a display that does not support primary |
| // home activity but it supports secondary home one. So replace it with secondary home. |
| Pair<ActivityInfo, Intent> info = mService.mRootWindowContainer |
| .resolveSecondaryHomeActivity(mUserId, mPresumableLaunchDisplayArea); |
| mIntent = info.second; |
| // The new task flag is needed because the home activity should already be in the root task |
| // and should not be moved to the caller's task. Also, activities cannot change their type, |
| // e.g. a standard activity cannot become a home activity. |
| mIntent.addFlags(FLAG_ACTIVITY_NEW_TASK); |
| mCallingPid = mRealCallingPid; |
| mCallingUid = mRealCallingUid; |
| mResolvedType = null; |
| |
| mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId, /* flags= */ 0, |
| mRealCallingUid, mRealCallingPid); |
| mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, /*profilerInfo=*/ null); |
| return true; |
| } |
| |
| private boolean isPackageSuspended() { |
| return mAInfo != null && mAInfo.applicationInfo != null |
| && (mAInfo.applicationInfo.flags & FLAG_SUSPENDED) != 0; |
| } |
| |
| /** |
| * Called when an activity is successfully launched. |
| */ |
| void onActivityLaunched(TaskInfo taskInfo, ActivityRecord r) { |
| final SparseArray<ActivityInterceptorCallback> callbacks = |
| mService.getActivityInterceptorCallbacks(); |
| ActivityInterceptorCallback.ActivityInterceptorInfo info = getInterceptorInfo( |
| r::clearOptionsAnimationForSiblings); |
| for (int i = 0; i < callbacks.size(); i++) { |
| final ActivityInterceptorCallback callback = callbacks.valueAt(i); |
| callback.onActivityLaunched(taskInfo, r.info, info); |
| } |
| } |
| |
| private ActivityInterceptorCallback.ActivityInterceptorInfo getInterceptorInfo( |
| @Nullable Runnable clearOptionsAnimation) { |
| return new ActivityInterceptorCallback.ActivityInterceptorInfo.Builder(mCallingUid, |
| mCallingPid, mRealCallingUid, mRealCallingPid, mUserId, mIntent, mRInfo, mAInfo) |
| .setResolvedType(mResolvedType) |
| .setCallingPackage(mCallingPackage) |
| .setCallingFeatureId(mCallingFeatureId) |
| .setCheckedOptions(mActivityOptions) |
| .setClearOptionsAnimationRunnable(clearOptionsAnimation) |
| .build(); |
| } |
| |
| } |