/*
|
* Copyright (C) 2018 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.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
|
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
|
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.view.Display.INVALID_DISPLAY;
|
import static android.app.ActivityTaskManager.INVALID_TASK_ID;
|
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
|
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
|
|
import android.annotation.Nullable;
|
import android.app.ActivityOptions;
|
import android.app.PendingIntent;
|
import android.content.Intent;
|
import android.content.pm.ActivityInfo;
|
import android.os.Binder;
|
import android.os.Bundle;
|
import android.os.Process;
|
import android.os.UserHandle;
|
import android.util.Slog;
|
import android.view.RemoteAnimationAdapter;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
/**
|
* Wraps {@link ActivityOptions}, records binder identity, and checks permission when retrieving
|
* the inner options. Also supports having two set of options: Once from the original caller, and
|
* once from the caller that is overriding it, which happens when sending a {@link PendingIntent}.
|
*/
|
public class SafeActivityOptions {
|
|
private static final String TAG = TAG_WITH_CLASS_NAME ? "SafeActivityOptions" : TAG_ATM;
|
|
private final int mOriginalCallingPid;
|
private final int mOriginalCallingUid;
|
private int mRealCallingPid;
|
private int mRealCallingUid;
|
private final @Nullable ActivityOptions mOriginalOptions;
|
private @Nullable ActivityOptions mCallerOptions;
|
|
/**
|
* Constructs a new instance from a bundle and records {@link Binder#getCallingPid}/
|
* {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
|
* this object.
|
*
|
* @param bOptions The {@link ActivityOptions} as {@link Bundle}.
|
*/
|
public static SafeActivityOptions fromBundle(Bundle bOptions) {
|
return bOptions != null
|
? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions))
|
: null;
|
}
|
|
/**
|
* Constructs a new instance and records {@link Binder#getCallingPid}/
|
* {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
|
* this object.
|
*
|
* @param options The options to wrap.
|
*/
|
public SafeActivityOptions(@Nullable ActivityOptions options) {
|
mOriginalCallingPid = Binder.getCallingPid();
|
mOriginalCallingUid = Binder.getCallingUid();
|
mOriginalOptions = options;
|
}
|
|
/**
|
* Overrides options with options from a caller and records {@link Binder#getCallingPid}/
|
* {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when calling this
|
* method.
|
*/
|
public void setCallerOptions(@Nullable ActivityOptions options) {
|
mRealCallingPid = Binder.getCallingPid();
|
mRealCallingUid = Binder.getCallingUid();
|
mCallerOptions = options;
|
}
|
|
/**
|
* Performs permission check and retrieves the options.
|
*
|
* @param r The record of the being started activity.
|
*/
|
ActivityOptions getOptions(ActivityRecord r) throws SecurityException {
|
return getOptions(r.intent, r.info, r.app, r.mStackSupervisor);
|
}
|
|
/**
|
* Performs permission check and retrieves the options when options are not being used to launch
|
* a specific activity (i.e. a task is moved to front).
|
*/
|
ActivityOptions getOptions(ActivityStackSupervisor supervisor) throws SecurityException {
|
return getOptions(null, null, null, supervisor);
|
}
|
|
/**
|
* Performs permission check and retrieves the options.
|
*
|
* @param intent The intent that is being launched.
|
* @param aInfo The info of the activity being launched.
|
* @param callerApp The record of the caller.
|
*/
|
ActivityOptions getOptions(@Nullable Intent intent, @Nullable ActivityInfo aInfo,
|
@Nullable WindowProcessController callerApp,
|
ActivityStackSupervisor supervisor) throws SecurityException {
|
if (mOriginalOptions != null) {
|
checkPermissions(intent, aInfo, callerApp, supervisor, mOriginalOptions,
|
mOriginalCallingPid, mOriginalCallingUid);
|
setCallingPidForRemoteAnimationAdapter(mOriginalOptions, mOriginalCallingPid);
|
}
|
if (mCallerOptions != null) {
|
checkPermissions(intent, aInfo, callerApp, supervisor, mCallerOptions,
|
mRealCallingPid, mRealCallingUid);
|
setCallingPidForRemoteAnimationAdapter(mCallerOptions, mRealCallingPid);
|
}
|
return mergeActivityOptions(mOriginalOptions, mCallerOptions);
|
}
|
|
private void setCallingPidForRemoteAnimationAdapter(ActivityOptions options, int callingPid) {
|
final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter();
|
if (adapter == null) {
|
return;
|
}
|
if (callingPid == Process.myPid()) {
|
Slog.wtf(TAG, "Safe activity options constructed after clearing calling id");
|
return;
|
}
|
adapter.setCallingPid(callingPid);
|
}
|
|
/**
|
* @see ActivityOptions#popAppVerificationBundle
|
*/
|
Bundle popAppVerificationBundle() {
|
return mOriginalOptions != null ? mOriginalOptions.popAppVerificationBundle() : null;
|
}
|
|
private void abort() {
|
if (mOriginalOptions != null) {
|
ActivityOptions.abort(mOriginalOptions);
|
}
|
if (mCallerOptions != null) {
|
ActivityOptions.abort(mCallerOptions);
|
}
|
}
|
|
static void abort(@Nullable SafeActivityOptions options) {
|
if (options != null) {
|
options.abort();
|
}
|
}
|
|
/**
|
* Merges two activity options into one, with {@code options2} taking precedence in case of a
|
* conflict.
|
*/
|
@VisibleForTesting
|
@Nullable ActivityOptions mergeActivityOptions(@Nullable ActivityOptions options1,
|
@Nullable ActivityOptions options2) {
|
if (options1 == null) {
|
return options2;
|
}
|
if (options2 == null) {
|
return options1;
|
}
|
final Bundle b1 = options1.toBundle();
|
final Bundle b2 = options2.toBundle();
|
b1.putAll(b2);
|
return ActivityOptions.fromBundle(b1);
|
}
|
|
private void checkPermissions(@Nullable Intent intent, @Nullable ActivityInfo aInfo,
|
@Nullable WindowProcessController callerApp, ActivityStackSupervisor supervisor,
|
ActivityOptions options, int callingPid, int callingUid) {
|
// If a launch task id is specified, then ensure that the caller is the recents
|
// component or has the START_TASKS_FROM_RECENTS permission
|
if (options.getLaunchTaskId() != INVALID_TASK_ID
|
&& !supervisor.mRecentTasks.isCallerRecents(callingUid)) {
|
final int startInTaskPerm = ActivityTaskManagerService.checkPermission(
|
START_TASKS_FROM_RECENTS, callingPid, callingUid);
|
if (startInTaskPerm == PERMISSION_DENIED) {
|
final String msg = "Permission Denial: starting " + getIntentString(intent)
|
+ " from " + callerApp + " (pid=" + callingPid
|
+ ", uid=" + callingUid + ") with launchTaskId="
|
+ options.getLaunchTaskId();
|
Slog.w(TAG, msg);
|
throw new SecurityException(msg);
|
}
|
}
|
// Check if someone tries to launch an activity on a private display with a different
|
// owner.
|
final int launchDisplayId = options.getLaunchDisplayId();
|
if (aInfo != null && launchDisplayId != INVALID_DISPLAY
|
&& !supervisor.isCallerAllowedToLaunchOnDisplay(callingPid, callingUid,
|
launchDisplayId, aInfo)) {
|
final String msg = "Permission Denial: starting " + getIntentString(intent)
|
+ " from " + callerApp + " (pid=" + callingPid
|
+ ", uid=" + callingUid + ") with launchDisplayId="
|
+ launchDisplayId;
|
Slog.w(TAG, msg);
|
throw new SecurityException(msg);
|
}
|
// Check if someone tries to launch an unwhitelisted activity into LockTask mode.
|
final boolean lockTaskMode = options.getLockTaskMode();
|
if (aInfo != null && lockTaskMode
|
&& !supervisor.mService.getLockTaskController().isPackageWhitelisted(
|
UserHandle.getUserId(callingUid), aInfo.packageName)) {
|
final String msg = "Permission Denial: starting " + getIntentString(intent)
|
+ " from " + callerApp + " (pid=" + callingPid
|
+ ", uid=" + callingUid + ") with lockTaskMode=true";
|
Slog.w(TAG, msg);
|
throw new SecurityException(msg);
|
}
|
|
// Check permission for remote animations
|
final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter();
|
if (adapter != null && supervisor.mService.checkPermission(
|
CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, callingPid, callingUid)
|
!= PERMISSION_GRANTED) {
|
final String msg = "Permission Denial: starting " + getIntentString(intent)
|
+ " from " + callerApp + " (pid=" + callingPid
|
+ ", uid=" + callingUid + ") with remoteAnimationAdapter";
|
Slog.w(TAG, msg);
|
throw new SecurityException(msg);
|
}
|
}
|
|
private String getIntentString(Intent intent) {
|
return intent != null ? intent.toString() : "(no intent)";
|
}
|
}
|