- In O, apps can request to enter picture-in-picture when the user
effectively leaves their activity for another task by default. To
prevent this from being abused, we need to add a setting for the
user to disable this behavior per-package in the system level.
When disabled, any activity from that package will only be able
to enter picture-in-picture when it is visible and resumed.
Bug:
34520451
Test: android.server.cts.ActivityManagerPinnedStackTests
Test: #testAppOpsDenyPipOnPause
Change-Id: Ib3a993e99ffb071706c6b7d3fb1c882b74acc5d7
Signed-off-by: Winson Chung <winsonc@google.com>
public static final int OP_READ_PHONE_NUMBER = 65;
/** @hide Request package installs through package installer */
public static final int OP_REQUEST_INSTALL_PACKAGES = 66;
+ /** @hide Enter picture-in-picture when hidden. */
+ public static final int OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE = 67;
/** @hide */
- public static final int _NUM_OP = 67;
+ public static final int _NUM_OP = 68;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
OP_AUDIO_ACCESSIBILITY_VOLUME,
OP_READ_PHONE_NUMBER,
OP_REQUEST_INSTALL_PACKAGES,
+ OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE,
};
/**
null, // OP_AUDIO_ACCESSIBILITY_VOLUME
OPSTR_READ_PHONE_NUMBER,
null, // OP_REQUEST_INSTALL_PACKAGES
+ null,
};
/**
"AUDIO_ACCESSIBILITY_VOLUME",
"READ_PHONE_NUMBER",
"REQUEST_INSTALL_PACKAGES",
+ "OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE",
};
/**
null, // no permission for changing accessibility volume
Manifest.permission.READ_PHONE_NUMBER,
Manifest.permission.REQUEST_INSTALL_PACKAGES,
+ null, // no permission for entering picture-in-picture on hide
};
/**
UserManager.DISALLOW_ADJUST_VOLUME, //AUDIO_ACCESSIBILITY_VOLUME
null, // READ_PHONE_NUMBER
null, // REQUEST_INSTALL_PACKAGES
+ null, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
};
/**
false, // AUDIO_ACCESSIBILITY_VOLUME
false, // READ_PHONE_NUMBER
false, // REQUEST_INSTALL_PACKAGES
+ false, // ENTER_PICTURE_IN_PICTURE_ON_HIDE
};
/**
AppOpsManager.MODE_ALLOWED, // OP_AUDIO_ACCESSIBILITY_VOLUME
AppOpsManager.MODE_ALLOWED,
AppOpsManager.MODE_DEFAULT, // OP_REQUEST_INSTALL_PACKAGES
+ AppOpsManager.MODE_ALLOWED, // OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE
};
/**
false, // OP_AUDIO_ACCESSIBILITY_VOLUME
false,
false, // OP_REQUEST_INSTALL_PACKAGES
+ false, // OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE
};
/**
= "android.settings.VR_LISTENER_SETTINGS";
/**
+ * Activity Action: Show Picture-in-picture settings.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_PICTURE_IN_PICTURE_SETTINGS
+ = "android.settings.PICTURE_IN_PICTURE_SETTINGS";
+
+ /**
* Activity Action: Show Storage Manager settings.
* <p>
* Input: Nothing.
// Indicates number of terms read on the terms screen.
PROVISIONING_TERMS_READ = 811;
+ // Logs that the user has edited the picture-in-picture settings.
+ // CATEGORY: SETTINGS
+ SETTINGS_MANAGE_PICTURE_IN_PICTURE = 812;
+
+ // ACTION: Allow "Enable picture-in-picture on hide" for an app
+ APP_PICTURE_IN_PICTURE_ON_HIDE_ALLOW = 813;
+
+ // ACTION: Deny "Enable picture-in-picture on hide" for an app
+ APP_PICTURE_IN_PICTURE_ON_HIDE_DENY = 814;
+
// ---- End O Constants, all O constants go above this line ----
// Add new aosp constants above this line.
import static android.app.ActivityManager.StackId.FREEFORM_WORKSPACE_STACK_ID;
import static android.app.ActivityManager.StackId.HOME_STACK_ID;
import static android.app.ActivityManager.StackId.PINNED_STACK_ID;
+import static android.app.AppOpsManager.MODE_ALLOWED;
+import static android.app.AppOpsManager.OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
import static android.content.pm.ActivityInfo.CONFIG_ORIENTATION;
import static android.content.pm.ActivityInfo.CONFIG_SCREEN_LAYOUT;
import android.annotation.NonNull;
import android.app.ActivityManager.TaskDescription;
import android.app.ActivityOptions;
+import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.app.PictureInPictureArgs;
import android.app.ResultInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Rect;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Debug;
import android.os.IBinder;
case PAUSED:
// When pausing, only allow enter PiP if not on the lockscreen and there is not
// already an existing PiP activity
- return !isKeyguardLocked && !hasPinnedStack && supportsPictureInPictureWhilePausing;
+ return !isKeyguardLocked && !hasPinnedStack && supportsPictureInPictureWhilePausing
+ && checkEnterPictureInPictureOnHideAppOpsState();
case STOPPING:
// When stopping in a valid state, then only allow enter PiP as in the pause state.
// Otherwise, fall through to throw an exception if the caller is trying to enter
// PiP in an invalid stopping state.
if (supportsPictureInPictureWhilePausing) {
- return !isKeyguardLocked && !hasPinnedStack;
+ return !isKeyguardLocked && !hasPinnedStack
+ && checkEnterPictureInPictureOnHideAppOpsState();
}
default:
throw new IllegalStateException(caller
}
}
+ /**
+ * @return Whether AppOps allows this package to enter picture-in-picture when it is hidden.
+ */
+ private boolean checkEnterPictureInPictureOnHideAppOpsState() {
+ try {
+ return service.getAppOpsService().checkOperation(OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE,
+ appInfo.uid, packageName) == MODE_ALLOWED;
+ } catch (RemoteException e) {
+ // Local call
+ }
+ return false;
+ }
+
boolean canGoInDockedStack() {
return !isHomeActivity() && isResizeableOrForced();
}