Merge "Properly handling situation when display is added and removed instantly" into udc-dev
diff --git a/core/java/android/app/admin/LockTaskPolicy.java b/core/java/android/app/admin/LockTaskPolicy.java
index f5d1cb4..b671d57 100644
--- a/core/java/android/app/admin/LockTaskPolicy.java
+++ b/core/java/android/app/admin/LockTaskPolicy.java
@@ -38,6 +38,7 @@
/**
* @hide
*/
+ // We default on the power button menu, in order to be consistent with pre-P behaviour
public static final int DEFAULT_LOCK_TASK_FLAG =
DevicePolicyManager.LOCK_TASK_FEATURE_GLOBAL_ACTIONS;
@@ -72,18 +73,28 @@
/**
* @hide
*/
- public LockTaskPolicy(@NonNull Set<String> packages) {
- Objects.requireNonNull(packages);
- mPackages.addAll(packages);
+ public LockTaskPolicy(@Nullable Set<String> packages) {
+ if (packages != null) {
+ mPackages.addAll(packages);
+ }
setValue(this);
}
/**
* @hide
*/
- public LockTaskPolicy(@NonNull Set<String> packages, int flags) {
- Objects.requireNonNull(packages);
- mPackages = new HashSet<>(packages);
+ public LockTaskPolicy(int flags) {
+ mFlags = flags;
+ setValue(this);
+ }
+
+ /**
+ * @hide
+ */
+ public LockTaskPolicy(@Nullable Set<String> packages, int flags) {
+ if (packages != null) {
+ mPackages.addAll(packages);
+ }
mFlags = flags;
setValue(this);
}
diff --git a/core/java/android/inputmethodservice/IInputMethodWrapper.java b/core/java/android/inputmethodservice/IInputMethodWrapper.java
index ed6a88f..70b72c8 100644
--- a/core/java/android/inputmethodservice/IInputMethodWrapper.java
+++ b/core/java/android/inputmethodservice/IInputMethodWrapper.java
@@ -304,7 +304,9 @@
return;
}
case DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT: {
- inputMethod.setStylusWindowIdleTimeoutForTest((long) msg.obj);
+ if (isValid(inputMethod, target, "DO_SET_STYLUS_WINDOW_IDLE_TIMEOUT")) {
+ inputMethod.setStylusWindowIdleTimeoutForTest((long) msg.obj);
+ }
return;
}
}
diff --git a/core/java/android/service/voice/AlwaysOnHotwordDetector.java b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
index 24c96ea..91c350a 100644
--- a/core/java/android/service/voice/AlwaysOnHotwordDetector.java
+++ b/core/java/android/service/voice/AlwaysOnHotwordDetector.java
@@ -1334,13 +1334,7 @@
@Override
public void destroy() {
synchronized (mLock) {
- if (mAvailability == STATE_KEYPHRASE_ENROLLED) {
- try {
- stopRecognition();
- } catch (Exception e) {
- Log.i(TAG, "failed to stopRecognition in destroy", e);
- }
- }
+ detachSessionLocked();
mAvailability = STATE_INVALID;
mIsAvailabilityOverriddenByTestApi = false;
@@ -1349,6 +1343,17 @@
super.destroy();
}
+ private void detachSessionLocked() {
+ try {
+ if (DBG) Slog.d(TAG, "detachSessionLocked() " + mSoundTriggerSession);
+ if (mSoundTriggerSession != null) {
+ mSoundTriggerSession.detach();
+ }
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
/**
* @hide
*/
diff --git a/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl b/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl
index 1ccc71a..23de50c 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionSoundTriggerSession.aidl
@@ -94,4 +94,9 @@
*/
@nullable SoundTrigger.ModelParamRange queryParameter(int keyphraseId,
in ModelParams modelParam);
+ /**
+ * Invalidates the sound trigger session and clears any associated resources. Subsequent calls
+ * to this object will throw IllegalStateException.
+ */
+ void detach();
}
diff --git a/core/res/res/values/config_telephony.xml b/core/res/res/values/config_telephony.xml
index a57a051..fd74185 100644
--- a/core/res/res/values/config_telephony.xml
+++ b/core/res/res/values/config_telephony.xml
@@ -133,6 +133,10 @@
<string name="config_pointing_ui_package" translatable="false"></string>
<java-symbol type="string" name="config_pointing_ui_package" />
+ <!-- Telephony pointing UI class name to be launched. -->
+ <string name="config_pointing_ui_class" translatable="false"></string>
+ <java-symbol type="string" name="config_pointing_ui_class" />
+
<!-- Telephony resends received satellite datagram to listener
if ack is not received within this timeout -->
<integer name="config_timeout_to_receive_delivered_ack_millis">300000</integer>
diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json
index eb2412c..596f351 100644
--- a/data/etc/services.core.protolog.json
+++ b/data/etc/services.core.protolog.json
@@ -1495,6 +1495,12 @@
"group": "WM_DEBUG_CONFIGURATION",
"at": "com\/android\/server\/wm\/ActivityRecord.java"
},
+ "-741766551": {
+ "message": "Content Recording: Ignoring session on invalid virtual display",
+ "level": "VERBOSE",
+ "group": "WM_DEBUG_CONTENT_RECORDING",
+ "at": "com\/android\/server\/wm\/ContentRecordingController.java"
+ },
"-732715767": {
"message": "Unable to retrieve window container to start recording for display %d",
"level": "VERBOSE",
diff --git a/media/java/android/media/projection/IMediaProjectionManager.aidl b/media/java/android/media/projection/IMediaProjectionManager.aidl
index 835e4c3..a3cd623 100644
--- a/media/java/android/media/projection/IMediaProjectionManager.aidl
+++ b/media/java/android/media/projection/IMediaProjectionManager.aidl
@@ -20,11 +20,23 @@
import android.media.projection.IMediaProjectionCallback;
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
+import android.media.projection.ReviewGrantedConsentResult;
import android.os.IBinder;
import android.view.ContentRecordingSession;
/** {@hide} */
interface IMediaProjectionManager {
+ /**
+ * Intent extra indicating if user must review access to the consent token already granted.
+ */
+ const String EXTRA_USER_REVIEW_GRANTED_CONSENT = "extra_media_projection_user_consent_required";
+
+ /**
+ * Intent extra indicating the package attempting to re-use granted consent.
+ */
+ const String EXTRA_PACKAGE_REUSING_GRANTED_CONSENT =
+ "extra_media_projection_package_reusing_consent";
+
@UnsupportedAppUsage
boolean hasProjectionPermission(int uid, String packageName);
@@ -37,6 +49,21 @@
boolean permanentGrant);
/**
+ * Returns the current {@link IMediaProjection} instance associated with the given
+ * package, or {@code null} if it is not possible to re-use the current projection.
+ *
+ * <p>Should only be invoked when the user has reviewed consent for a re-used projection token.
+ * Requires that there is a prior session waiting for the user to review consent, and the given
+ * package details match those on the current projection.
+ *
+ * @see {@link #isCurrentProjection}
+ */
+ @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
+ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ + ".permission.MANAGE_MEDIA_PROJECTION)")
+ IMediaProjection getProjection(int uid, String packageName);
+
+ /**
* Returns {@code true} if the given {@link IMediaProjection} corresponds to the current
* projection, or {@code false} otherwise.
*/
@@ -58,7 +85,7 @@
*/
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
- void requestConsentForInvalidProjection(IMediaProjection projection);
+ void requestConsentForInvalidProjection(in IMediaProjection projection);
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
@@ -94,9 +121,32 @@
*
* @param incomingSession the nullable incoming content recording session
* @param projection the non-null projection the session describes
+ * @throws SecurityException If the provided projection is not current.
*/
@JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ ".permission.MANAGE_MEDIA_PROJECTION)")
boolean setContentRecordingSession(in ContentRecordingSession incomingSession,
in IMediaProjection projection);
+
+ /**
+ * Sets the result of the user reviewing the recording permission, when the host app is re-using
+ * the consent token.
+ *
+ * <p>Ignores the provided result if the given projection is not the current projection.
+ *
+ * <p>Based on the given result:
+ * <ul>
+ * <li>If UNKNOWN or RECORD_CANCEL, then tear down the recording.</li>
+ * <li>If RECORD_CONTENT_DISPLAY, then record the default display.</li>
+ * <li>If RECORD_CONTENT_TASK, record the task indicated by
+ * {@link IMediaProjection#getLaunchCookie}.</li>
+ * </ul>
+ * @param projection The projection associated with the consent result. Must be the current
+ * projection instance, unless the given result is RECORD_CANCEL.
+ */
+ @EnforcePermission("android.Manifest.permission.MANAGE_MEDIA_PROJECTION")
+ @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest"
+ + ".permission.MANAGE_MEDIA_PROJECTION)")
+ void setUserReviewGrantedConsentResult(ReviewGrantedConsentResult consentResult,
+ in @nullable IMediaProjection projection);
}
diff --git a/media/java/android/media/projection/ReviewGrantedConsentResult.aidl b/media/java/android/media/projection/ReviewGrantedConsentResult.aidl
new file mode 100644
index 0000000..4f25be7
--- /dev/null
+++ b/media/java/android/media/projection/ReviewGrantedConsentResult.aidl
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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 android.media.projection;
+
+/**
+ * Indicates result of user interacting with consent dialog, when their review is required due to
+ * app re-using the token.
+
+ * @hide
+ */
+@Backing(type="int")
+enum ReviewGrantedConsentResult {
+ UNKNOWN = -1,
+ RECORD_CANCEL = 0,
+ RECORD_CONTENT_DISPLAY = 1,
+ RECORD_CONTENT_TASK = 2,
+}
diff --git a/media/java/android/media/tv/ITvInputSessionWrapper.java b/media/java/android/media/tv/ITvInputSessionWrapper.java
index 80a3e70..d749b91 100644
--- a/media/java/android/media/tv/ITvInputSessionWrapper.java
+++ b/media/java/android/media/tv/ITvInputSessionWrapper.java
@@ -223,7 +223,8 @@
break;
}
case DO_TIME_SHIFT_SET_MODE: {
- mTvInputSessionImpl.timeShiftSetMode((Integer) msg.obj);
+ mTvInputSessionImpl.timeShiftSetMode(msg.arg1);
+ break;
}
case DO_TIME_SHIFT_ENABLE_POSITION_TRACKING: {
mTvInputSessionImpl.timeShiftEnablePositionTracking((Boolean) msg.obj);
diff --git a/packages/SettingsLib/Spa/build.gradle b/packages/SettingsLib/Spa/build.gradle
index 4fd2b5d..e68ef85 100644
--- a/packages/SettingsLib/Spa/build.gradle
+++ b/packages/SettingsLib/Spa/build.gradle
@@ -14,17 +14,27 @@
* limitations under the License.
*/
+import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+
buildscript {
ext {
BUILD_TOOLS_VERSION = "30.0.3"
MIN_SDK = 21
TARGET_SDK = 33
jetpack_compose_version = '1.4.0-beta01'
- jetpack_compose_compiler_version = '1.4.0'
+ jetpack_compose_compiler_version = '1.4.4'
}
}
plugins {
- id 'com.android.application' version '8.0.0-beta05' apply false
- id 'com.android.library' version '8.0.0-beta05' apply false
- id 'org.jetbrains.kotlin.android' version '1.8.0' apply false
+ id 'com.android.application' version '8.0.0' apply false
+ id 'com.android.library' version '8.0.0' apply false
+ id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
+}
+subprojects {
+ tasks.withType(KotlinCompile).configureEach {
+ kotlinOptions {
+ jvmTarget = "17"
+ freeCompilerArgs = ["-Xjvm-default=all"]
+ }
+ }
}
diff --git a/packages/SettingsLib/Spa/gallery/build.gradle b/packages/SettingsLib/Spa/gallery/build.gradle
index 416a403..212aa7b 100644
--- a/packages/SettingsLib/Spa/gallery/build.gradle
+++ b/packages/SettingsLib/Spa/gallery/build.gradle
@@ -42,12 +42,8 @@
}
}
compileOptions {
- sourceCompatibility JavaVersion.VERSION_11
- targetCompatibility JavaVersion.VERSION_11
- }
- kotlinOptions {
- jvmTarget = '11'
- freeCompilerArgs = ["-Xjvm-default=all"]
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
}
buildFeatures {
compose true
diff --git a/packages/SettingsLib/Spa/spa/build.gradle b/packages/SettingsLib/Spa/spa/build.gradle
index 9962c93..fb945a3 100644
--- a/packages/SettingsLib/Spa/spa/build.gradle
+++ b/packages/SettingsLib/Spa/spa/build.gradle
@@ -51,10 +51,6 @@
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = '17'
- freeCompilerArgs = ["-Xjvm-default=all"]
- }
buildFeatures {
compose true
}
@@ -79,7 +75,7 @@
api "androidx.compose.ui:ui-tooling-preview:$jetpack_compose_version"
api "androidx.lifecycle:lifecycle-livedata-ktx"
api "androidx.lifecycle:lifecycle-runtime-compose"
- api "androidx.navigation:navigation-compose:2.6.0-alpha07"
+ api "androidx.navigation:navigation-compose:2.6.0-alpha08"
api "com.github.PhilJay:MPAndroidChart:v3.1.0-alpha"
api "com.google.android.material:material:1.7.0-alpha03"
debugApi "androidx.compose.ui:ui-tooling:$jetpack_compose_version"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
index f0df9a6..8cbf7cc 100644
--- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Spinner.kt
@@ -19,7 +19,6 @@
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material.icons.Icons
@@ -40,7 +39,6 @@
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.theme.SettingsTheme
@@ -60,13 +58,17 @@
Box(
modifier = Modifier
- .padding(SettingsDimension.itemPadding)
+ .padding(
+ start = SettingsDimension.itemPaddingStart,
+ top = SettingsDimension.itemPaddingAround,
+ end = SettingsDimension.itemPaddingEnd,
+ bottom = SettingsDimension.itemPaddingAround,
+ )
.selectableGroup(),
) {
val contentPadding = PaddingValues(horizontal = SettingsDimension.itemPaddingEnd)
Button(
onClick = { expanded = true },
- modifier = Modifier.height(36.dp),
colors = ButtonDefaults.buttonColors(
containerColor = SettingsTheme.colorScheme.spinnerHeaderContainer,
contentColor = SettingsTheme.colorScheme.onSpinnerHeaderContainer,
@@ -86,7 +88,6 @@
expanded = expanded,
onDismissRequest = { expanded = false },
modifier = Modifier.background(SettingsTheme.colorScheme.spinnerItemContainer),
- offset = DpOffset(x = 0.dp, y = 4.dp),
) {
for (option in options) {
DropdownMenuItem(
@@ -116,7 +117,9 @@
) {
Text(
text = option?.text ?: "",
- modifier = modifier.padding(end = SettingsDimension.itemPaddingEnd),
+ modifier = modifier
+ .padding(end = SettingsDimension.itemPaddingEnd)
+ .padding(vertical = SettingsDimension.itemPaddingAround),
color = color,
style = MaterialTheme.typography.labelLarge,
)
diff --git a/packages/SettingsLib/Spa/testutils/build.gradle b/packages/SettingsLib/Spa/testutils/build.gradle
index e7f7db2..23a9add 100644
--- a/packages/SettingsLib/Spa/testutils/build.gradle
+++ b/packages/SettingsLib/Spa/testutils/build.gradle
@@ -41,10 +41,6 @@
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
- kotlinOptions {
- jvmTarget = '17'
- freeCompilerArgs = ["-Xjvm-default=all"]
- }
buildFeatures {
compose true
}
diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
index 9adfcc9..99d4662 100644
--- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java
@@ -374,11 +374,10 @@
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
final boolean captured = (mIsSwiping || mLongPressSent || mMenuRowIntercepting);
- mIsSwiping = false;
- mTouchedView = null;
mLongPressSent = false;
mCallback.onLongPressSent(null);
mMenuRowIntercepting = false;
+ resetSwipeState();
cancelLongPress();
if (captured) return true;
break;
@@ -491,7 +490,7 @@
}
if (!mCancelled || wasRemoved) {
mCallback.onChildDismissed(animView);
- resetSwipeState();
+ resetSwipeOfView(animView);
}
if (endAction != null) {
endAction.accept(mCancelled);
@@ -546,7 +545,7 @@
if (!cancelled) {
updateSwipeProgressFromOffset(animView, canBeDismissed);
- resetSwipeState();
+ resetSwipeOfView(animView);
}
onChildSnappedBack(animView, targetLeft);
});
@@ -806,9 +805,20 @@
return mIsSwiping ? mTouchedView : null;
}
+ protected void resetSwipeOfView(View view) {
+ if (getSwipedView() == view) {
+ resetSwipeState();
+ }
+ }
+
public void resetSwipeState() {
+ View swipedView = getSwipedView();
mTouchedView = null;
mIsSwiping = false;
+ if (swipedView != null) {
+ snapChildIfNeeded(swipedView, false, 0);
+ onChildSnappedBack(swipedView, 0);
+ }
}
private float getTouchSlop(MotionEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 52d4171..0860c20 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -20,7 +20,10 @@
import android.content.res.Configuration
import android.content.res.Resources
import android.media.projection.IMediaProjection
+import android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT
import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION
+import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL
+import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK
import android.os.Binder
import android.os.Bundle
import android.os.IBinder
@@ -67,6 +70,11 @@
private lateinit var controller: MediaProjectionAppSelectorController
private lateinit var recentsViewController: MediaProjectionRecentsViewController
private lateinit var component: MediaProjectionAppSelectorComponent
+ // Indicate if we are under the media projection security flow
+ // i.e. when a host app reuses consent token, review the permission and update it to the service
+ private var reviewGrantedConsentRequired = false
+ // If an app is selected, set to true so that we don't send RECORD_CANCEL in onDestroy
+ private var taskSelected = false
override fun getLayoutResource() = R.layout.media_projection_app_selector
@@ -85,6 +93,9 @@
component.personalProfileUserHandle
)
+ reviewGrantedConsentRequired =
+ intent.getBooleanExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, false)
+
super.onCreate(bundle)
controller.init()
}
@@ -149,6 +160,16 @@
}
override fun onDestroy() {
+ // onDestroy is also called when an app is selected, in that case we only want to send
+ // RECORD_CONTENT_TASK but not RECORD_CANCEL
+ if (!taskSelected) {
+ // TODO(b/272010156): Return result to PermissionActivity and update service there
+ MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
+ RECORD_CANCEL,
+ reviewGrantedConsentRequired,
+ /* projection= */ null
+ )
+ }
activityLauncher.destroy()
controller.destroy()
super.onDestroy()
@@ -163,6 +184,7 @@
}
override fun returnSelectedApp(launchCookie: IBinder) {
+ taskSelected = true
if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
// The client requested to return the result in the result receiver instead of
// activity result, let's send the media projection to the result receiver
@@ -174,7 +196,11 @@
val captureRegion = MediaProjectionCaptureTarget(launchCookie)
val data = Bundle().apply { putParcelable(KEY_CAPTURE_TARGET, captureRegion) }
resultReceiver.send(RESULT_OK, data)
+ // TODO(b/279175710): Ensure consent result is always set here. Skipping this for now
+ // in ScreenMediaRecorder, since we know the permission grant (projection) is never
+ // reused in that scenario.
} else {
+ // TODO(b/272010156): Return result to PermissionActivity and update service there
// Return the media projection instance as activity result
val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION)
val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder)
@@ -185,6 +211,11 @@
intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder())
setResult(RESULT_OK, intent)
setForceSendResultForMediaProjection()
+ MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
+ RECORD_CONTENT_TASK,
+ reviewGrantedConsentRequired,
+ projection
+ )
}
finish()
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
index ccddd1d..e217e36 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionPermissionActivity.java
@@ -16,11 +16,16 @@
package com.android.systemui.media;
+import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
+import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
import static com.android.systemui.screenrecord.ScreenShareOptionKt.ENTIRE_SCREEN;
import static com.android.systemui.screenrecord.ScreenShareOptionKt.SINGLE_APP;
+import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
@@ -30,12 +35,10 @@
import android.content.pm.PackageManager;
import android.graphics.Typeface;
import android.media.projection.IMediaProjection;
-import android.media.projection.IMediaProjectionManager;
import android.media.projection.MediaProjectionManager;
+import android.media.projection.ReviewGrantedConsentResult;
import android.os.Bundle;
-import android.os.IBinder;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.UserHandle;
import android.text.BidiFormatter;
import android.text.SpannableString;
@@ -55,10 +58,10 @@
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.Utils;
-import javax.inject.Inject;
-
import dagger.Lazy;
+import javax.inject.Inject;
+
public class MediaProjectionPermissionActivity extends Activity
implements DialogInterface.OnClickListener {
private static final String TAG = "MediaProjectionPermissionActivity";
@@ -70,10 +73,13 @@
private String mPackageName;
private int mUid;
- private IMediaProjectionManager mService;
private AlertDialog mDialog;
+ // Indicates if user must review already-granted consent that the MediaProjection app is
+ // attempting to re-use.
+ private boolean mReviewGrantedConsentRequired = false;
+
@Inject
public MediaProjectionPermissionActivity(FeatureFlags featureFlags,
Lazy<ScreenCaptureDevicePolicyResolver> screenCaptureDevicePolicyResolver) {
@@ -85,13 +91,23 @@
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
- mPackageName = getCallingPackage();
- IBinder b = ServiceManager.getService(MEDIA_PROJECTION_SERVICE);
- mService = IMediaProjectionManager.Stub.asInterface(b);
+ final Intent launchingIntent = getIntent();
+ mReviewGrantedConsentRequired = launchingIntent.getBooleanExtra(
+ EXTRA_USER_REVIEW_GRANTED_CONSENT, false);
+ mPackageName = getCallingPackage();
+
+ // This activity is launched directly by an app, or system server. System server provides
+ // the package name through the intent if so.
if (mPackageName == null) {
- finish();
- return;
+ if (launchingIntent.hasExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT)) {
+ mPackageName = launchingIntent.getStringExtra(
+ EXTRA_PACKAGE_REUSING_GRANTED_CONSENT);
+ } else {
+ setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
+ return;
+ }
}
PackageManager packageManager = getPackageManager();
@@ -100,25 +116,36 @@
aInfo = packageManager.getApplicationInfo(mPackageName, 0);
mUid = aInfo.uid;
} catch (PackageManager.NameNotFoundException e) {
- Log.e(TAG, "unable to look up package name", e);
- finish();
+ Log.e(TAG, "Unable to look up package name", e);
+ setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
return;
}
try {
- if (mService.hasProjectionPermission(mUid, mPackageName)) {
- setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
- finish();
+ if (MediaProjectionServiceHelper.hasProjectionPermission(mUid, mPackageName)) {
+ final IMediaProjection projection =
+ MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
+ mReviewGrantedConsentRequired);
+ // Automatically grant consent if a system-privileged component is recording.
+ final Intent intent = new Intent();
+ intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
+ projection.asBinder());
+ setResult(RESULT_OK, intent);
+ finish(RECORD_CONTENT_DISPLAY, projection);
return;
}
} catch (RemoteException e) {
Log.e(TAG, "Error checking projection permissions", e);
- finish();
+ setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
return;
}
if (mFeatureFlags.isEnabled(Flags.WM_ENABLE_PARTIAL_SCREEN_SHARING_ENTERPRISE_POLICIES)) {
if (showScreenCaptureDisabledDialogIfNeeded()) {
+ setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
return;
}
}
@@ -178,7 +205,7 @@
ScreenShareOption selectedOption =
((MediaProjectionPermissionDialog) mDialog).getSelectedScreenShareOption();
grantMediaProjectionPermission(selectedOption.getMode());
- }, appName);
+ }, () -> finish(RECORD_CANCEL, /* projection= */ null), appName);
} else {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this,
R.style.Theme_SystemUI_Dialog)
@@ -191,7 +218,6 @@
}
setUpDialog(mDialog);
-
mDialog.show();
}
@@ -207,6 +233,12 @@
public void onClick(DialogInterface dialog, int which) {
if (which == AlertDialog.BUTTON_POSITIVE) {
grantMediaProjectionPermission(ENTIRE_SCREEN);
+ } else {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ }
+ setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
}
}
@@ -240,15 +272,25 @@
private void grantMediaProjectionPermission(int screenShareMode) {
try {
if (screenShareMode == ENTIRE_SCREEN) {
- setResult(RESULT_OK, getMediaProjectionIntent(mUid, mPackageName));
+ final IMediaProjection projection =
+ MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName,
+ mReviewGrantedConsentRequired);
+ final Intent intent = new Intent();
+ intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
+ projection.asBinder());
+ setResult(RESULT_OK, intent);
+ finish(RECORD_CONTENT_DISPLAY, projection);
}
if (isPartialScreenSharingEnabled() && screenShareMode == SINGLE_APP) {
- IMediaProjection projection = createProjection(mUid, mPackageName);
- final Intent intent = new Intent(this, MediaProjectionAppSelectorActivity.class);
+ IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection(
+ mUid, mPackageName, mReviewGrantedConsentRequired);
+ final Intent intent = new Intent(this,
+ MediaProjectionAppSelectorActivity.class);
intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION,
projection.asBinder());
intent.putExtra(MediaProjectionAppSelectorActivity.EXTRA_HOST_APP_USER_HANDLE,
getHostUserHandle());
+ intent.putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, mReviewGrantedConsentRequired);
intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
// Start activity from the current foreground user to avoid creating a separate
@@ -259,11 +301,11 @@
} catch (RemoteException e) {
Log.e(TAG, "Error granting projection permission", e);
setResult(RESULT_CANCELED);
+ finish(RECORD_CANCEL, /* projection= */ null);
} finally {
if (mDialog != null) {
mDialog.dismiss();
}
- finish();
}
}
@@ -271,22 +313,22 @@
return UserHandle.getUserHandleForUid(getLaunchedFromUid());
}
- private IMediaProjection createProjection(int uid, String packageName) throws RemoteException {
- return mService.createProjection(uid, packageName,
- MediaProjectionManager.TYPE_SCREEN_CAPTURE, false /* permanentGrant */);
+ @Override
+ public void finish() {
+ // Default to cancelling recording when user needs to review consent.
+ finish(RECORD_CANCEL, /* projection= */ null);
}
- private Intent getMediaProjectionIntent(int uid, String packageName)
- throws RemoteException {
- IMediaProjection projection = createProjection(uid, packageName);
- Intent intent = new Intent();
- intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, projection.asBinder());
- return intent;
+ private void finish(@ReviewGrantedConsentResult int consentResult,
+ @Nullable IMediaProjection projection) {
+ MediaProjectionServiceHelper.setReviewedConsentIfNeeded(
+ consentResult, mReviewGrantedConsentRequired, projection);
+ super.finish();
}
private void onDialogDismissedOrCancelled(DialogInterface dialogInterface) {
if (!isFinishing()) {
- finish();
+ finish(RECORD_CANCEL, /* projection= */ null);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt
new file mode 100644
index 0000000..9e616e2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionServiceHelper.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2023 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.systemui.media
+
+import android.content.Context
+import android.media.projection.IMediaProjection
+import android.media.projection.IMediaProjectionManager
+import android.media.projection.MediaProjectionManager
+import android.media.projection.ReviewGrantedConsentResult
+import android.os.RemoteException
+import android.os.ServiceManager
+import android.util.Log
+
+/**
+ * Helper class that handles the media projection service related actions. It simplifies invoking
+ * the MediaProjectionManagerService and updating the permission consent.
+ */
+class MediaProjectionServiceHelper {
+ companion object {
+ private const val TAG = "MediaProjectionServiceHelper"
+ private val service =
+ IMediaProjectionManager.Stub.asInterface(
+ ServiceManager.getService(Context.MEDIA_PROJECTION_SERVICE)
+ )
+
+ @JvmStatic
+ @Throws(RemoteException::class)
+ fun hasProjectionPermission(uid: Int, packageName: String) =
+ service.hasProjectionPermission(uid, packageName)
+
+ @JvmStatic
+ @Throws(RemoteException::class)
+ fun createOrReuseProjection(
+ uid: Int,
+ packageName: String,
+ reviewGrantedConsentRequired: Boolean
+ ): IMediaProjection {
+ val existingProjection =
+ if (reviewGrantedConsentRequired) service.getProjection(uid, packageName) else null
+ return existingProjection
+ ?: service.createProjection(
+ uid,
+ packageName,
+ MediaProjectionManager.TYPE_SCREEN_CAPTURE,
+ false /* permanentGrant */
+ )
+ }
+
+ /**
+ * This method is called when a host app reuses the consent token. If the token is being
+ * used more than once, ask the user to review their consent and send the reviewed result.
+ *
+ * @param consentResult consent result to update
+ * @param reviewGrantedConsentRequired if user must review already-granted consent that the
+ * host app is attempting to reuse
+ * @param projection projection token associated with the consent result, or null if the
+ * result is for cancelling.
+ */
+ @JvmStatic
+ fun setReviewedConsentIfNeeded(
+ @ReviewGrantedConsentResult consentResult: Int,
+ reviewGrantedConsentRequired: Boolean,
+ projection: IMediaProjection?
+ ) {
+ // Only send the result to the server, when the user needed to review the re-used
+ // consent token.
+ if (
+ reviewGrantedConsentRequired && consentResult != ReviewGrantedConsentResult.UNKNOWN
+ ) {
+ try {
+ service.setUserReviewGrantedConsentResult(consentResult, projection)
+ } catch (e: RemoteException) {
+ // If we are unable to pass back the result, capture continues with blank frames
+ Log.e(TAG, "Unable to set required consent result for token re-use", e)
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
index db0052a..f63bf07 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/BaseScreenSharePermissionDialog.kt
@@ -43,6 +43,7 @@
) : SystemUIDialog(context), AdapterView.OnItemSelectedListener {
private lateinit var dialogTitle: TextView
private lateinit var startButton: TextView
+ private lateinit var cancelButton: TextView
private lateinit var warning: TextView
private lateinit var screenShareModeSpinner: Spinner
var selectedScreenShareOption: ScreenShareOption = screenShareOptions.first()
@@ -57,7 +58,7 @@
dialogTitle = findViewById(R.id.screen_share_dialog_title)
warning = findViewById(R.id.text_warning)
startButton = findViewById(R.id.button_start)
- findViewById<TextView>(R.id.button_cancel).setOnClickListener { dismiss() }
+ cancelButton = findViewById(R.id.button_cancel)
updateIcon()
initScreenShareOptions()
createOptionsView(getOptionsViewLayoutId())
@@ -117,6 +118,10 @@
startButton.setOnClickListener(listener)
}
+ protected fun setCancelButtonOnClickListener(listener: View.OnClickListener?) {
+ cancelButton.setOnClickListener(listener)
+ }
+
// Create additional options that is shown under the share mode spinner
// Eg. the audio and tap toggles in SysUI Recorder
@LayoutRes protected open fun getOptionsViewLayoutId(): Int? = null
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
index c5a82ce1..201557c 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/MediaProjectionPermissionDialog.kt
@@ -23,6 +23,7 @@
class MediaProjectionPermissionDialog(
context: Context?,
private val onStartRecordingClicked: Runnable,
+ private val onCancelClicked: Runnable,
private val appName: String?
) : BaseScreenSharePermissionDialog(context, createOptionList(appName), appName) {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -39,6 +40,10 @@
onStartRecordingClicked.run()
dismiss()
}
+ setCancelButtonOnClickListener {
+ onCancelClicked.run()
+ dismiss()
+ }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 24e8f39..82608b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -4189,10 +4189,7 @@
mCentralSurfaces.resetUserExpandedStates();
clearTemporaryViews();
clearUserLockedViews();
- if (mSwipeHelper.isSwiping()) {
- mSwipeHelper.resetSwipeState();
- updateContinuousShadowDrawing();
- }
+ cancelActiveSwipe();
}
}
@@ -4264,6 +4261,9 @@
if (!mIsExpanded) {
mGroupExpansionManager.collapseGroups();
mExpandHelper.cancelImmediately();
+ if (!mIsExpansionChanging) {
+ cancelActiveSwipe();
+ }
}
updateNotificationAnimationStates();
updateChronometers();
@@ -6113,7 +6113,11 @@
}
}
- @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
+ private void cancelActiveSwipe() {
+ mSwipeHelper.resetSwipeState();
+ updateContinuousShadowDrawing();
+ }
+
void updateContinuousShadowDrawing() {
boolean continuousShadowUpdate = mAnimationRunning
|| mSwipeHelper.isSwiping();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 2387495..e1c8064 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -3640,9 +3640,6 @@
/* wakingUp= */ true,
mShouldDelayWakeUpAnimation);
- if (!mKeyguardBypassController.getBypassEnabled()) {
- mHeadsUpManager.releaseAllImmediately();
- }
updateVisibleToUser();
updateIsKeyguard();
mDozeServiceHost.stopDozing();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 7153e59..f771606 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -801,6 +801,34 @@
}
@Test
+ public void onShadeClosesWithAnimationWillResetSwipeState() {
+ // GIVEN shade is expanded
+ mStackScroller.setIsExpanded(true);
+ clearInvocations(mNotificationSwipeHelper);
+
+ // WHEN closing the shade with the animations
+ mStackScroller.onExpansionStarted();
+ mStackScroller.setIsExpanded(false);
+ mStackScroller.onExpansionStopped();
+
+ // VERIFY swipe is reset
+ verify(mNotificationSwipeHelper).resetSwipeState();
+ }
+
+ @Test
+ public void onShadeClosesWithoutAnimationWillResetSwipeState() {
+ // GIVEN shade is expanded
+ mStackScroller.setIsExpanded(true);
+ clearInvocations(mNotificationSwipeHelper);
+
+ // WHEN closing the shade without the animation
+ mStackScroller.setIsExpanded(false);
+
+ // VERIFY swipe is reset
+ verify(mNotificationSwipeHelper).resetSwipeState();
+ }
+
+ @Test
public void testSplitShade_hasTopOverscroll() {
mTestableResources
.addOverride(R.bool.config_use_split_notification_shade, /* value= */ true);
diff --git a/services/core/java/com/android/server/SoundTriggerInternal.java b/services/core/java/com/android/server/SoundTriggerInternal.java
index e6c1750..6529465 100644
--- a/services/core/java/com/android/server/SoundTriggerInternal.java
+++ b/services/core/java/com/android/server/SoundTriggerInternal.java
@@ -142,6 +142,12 @@
@ModelParams int modelParam);
/**
+ * Invalidates the sound trigger session and clears any associated resources. Subsequent
+ * calls to this object will throw IllegalStateException.
+ */
+ void detach();
+
+ /**
* Unloads (and stops if running) the given keyphraseId
*/
int unloadKeyphraseModel(int keyphaseId);
diff --git a/services/core/java/com/android/server/input/KeyboardBacklightController.java b/services/core/java/com/android/server/input/KeyboardBacklightController.java
index 048308e..48c346a 100644
--- a/services/core/java/com/android/server/input/KeyboardBacklightController.java
+++ b/services/core/java/com/android/server/input/KeyboardBacklightController.java
@@ -199,8 +199,11 @@
}
if (brightness.isPresent()) {
int brightnessValue = Math.max(0, Math.min(MAX_BRIGHTNESS, brightness.getAsInt()));
- int brightnessLevel = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
- updateBacklightState(inputDevice.getId(), keyboardBacklight, brightnessLevel,
+ int index = Arrays.binarySearch(BRIGHTNESS_VALUE_FOR_LEVEL, brightnessValue);
+ if (index < 0) {
+ index = Math.min(NUM_BRIGHTNESS_CHANGE_STEPS, -(index + 1));
+ }
+ updateBacklightState(inputDevice.getId(), keyboardBacklight, index,
false /* isTriggeredByKeyPress */);
if (DEBUG) {
Slog.d(TAG, "Restoring brightness level " + brightness.getAsInt());
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 72c7dad..d8716b3 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -16,6 +16,8 @@
package com.android.server.input;
+import android.annotation.AnyThread;
+import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
@@ -99,6 +101,7 @@
private static final int MSG_SWITCH_KEYBOARD_LAYOUT = 2;
private static final int MSG_RELOAD_KEYBOARD_LAYOUTS = 3;
private static final int MSG_UPDATE_KEYBOARD_LAYOUTS = 4;
+ private static final int MSG_CURRENT_IME_INFO_CHANGED = 5;
private final Context mContext;
private final NativeInputManagerService mNative;
@@ -108,16 +111,17 @@
private final Handler mHandler;
// Connected keyboards with associated keyboard layouts (either auto-detected or manually
- // selected layout). If the mapped value is null/empty, it means that no layout has been
- // configured for the keyboard and user might need to manually configure it from the Settings.
- private final SparseArray<Set<String>> mConfiguredKeyboards = new SparseArray<>();
+ // selected layout).
+ private final SparseArray<KeyboardConfiguration> mConfiguredKeyboards = new SparseArray<>();
private Toast mSwitchedKeyboardLayoutToast;
// This cache stores "best-matched" layouts so that we don't need to run the matching
// algorithm repeatedly.
@GuardedBy("mKeyboardLayoutCache")
private final Map<String, String> mKeyboardLayoutCache = new ArrayMap<>();
+ private final Object mImeInfoLock = new Object();
@Nullable
+ @GuardedBy("mImeInfoLock")
private ImeInfo mCurrentImeInfo;
KeyboardLayoutManager(Context context, NativeInputManagerService nativeService,
@@ -155,26 +159,32 @@
}
@Override
+ @MainThread
public void onInputDeviceAdded(int deviceId) {
onInputDeviceChanged(deviceId);
- if (useNewSettingsUi()) {
- // Force native callback to set up keyboard layout overlay for newly added keyboards
- reloadKeyboardLayouts();
- }
}
@Override
+ @MainThread
public void onInputDeviceRemoved(int deviceId) {
mConfiguredKeyboards.remove(deviceId);
maybeUpdateNotification();
}
@Override
+ @MainThread
public void onInputDeviceChanged(int deviceId) {
final InputDevice inputDevice = getInputDevice(deviceId);
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return;
}
+ KeyboardConfiguration config = mConfiguredKeyboards.get(deviceId);
+ if (config == null) {
+ config = new KeyboardConfiguration();
+ mConfiguredKeyboards.put(deviceId, config);
+ }
+
+ boolean needToShowNotification = false;
if (!useNewSettingsUi()) {
synchronized (mDataStore) {
String layout = getCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier());
@@ -182,54 +192,66 @@
layout = getDefaultKeyboardLayout(inputDevice);
if (layout != null) {
setCurrentKeyboardLayoutForInputDevice(inputDevice.getIdentifier(), layout);
- } else {
- mConfiguredKeyboards.put(inputDevice.getId(), new HashSet<>());
}
}
+ config.setCurrentLayout(layout);
+ if (layout == null) {
+ // In old settings show notification always until user manually selects a
+ // layout in the settings.
+ needToShowNotification = true;
+ }
}
} else {
final InputDeviceIdentifier identifier = inputDevice.getIdentifier();
final String key = getLayoutDescriptor(identifier);
Set<String> selectedLayouts = new HashSet<>();
- boolean needToShowMissingLayoutNotification = false;
for (ImeInfo imeInfo : getImeInfoListForLayoutMapping()) {
// Check if the layout has been previously configured
String layout = getKeyboardLayoutForInputDeviceInternal(identifier,
new ImeInfo(imeInfo.mUserId, imeInfo.mImeSubtypeHandle,
imeInfo.mImeSubtype));
if (layout == null) {
- needToShowMissingLayoutNotification = true;
- continue;
+ // If even one layout not configured properly, we need to ask user to configure
+ // the keyboard properly from the Settings.
+ selectedLayouts.clear();
+ break;
}
selectedLayouts.add(layout);
}
- if (needToShowMissingLayoutNotification) {
- // If even one layout not configured properly we will show configuration
- // notification allowing user to set the keyboard layout.
- selectedLayouts.clear();
- }
-
if (DEBUG) {
Slog.d(TAG,
"Layouts selected for input device: " + identifier + " -> selectedLayouts: "
+ selectedLayouts);
}
- mConfiguredKeyboards.set(inputDevice.getId(), selectedLayouts);
+
+ config.setConfiguredLayouts(selectedLayouts);
+
+ // Update current layout: If there is a change then need to reload.
+ synchronized (mImeInfoLock) {
+ String layout = getKeyboardLayoutForInputDeviceInternal(
+ inputDevice.getIdentifier(), mCurrentImeInfo);
+ if (!Objects.equals(layout, config.getCurrentLayout())) {
+ config.setCurrentLayout(layout);
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ }
+ }
synchronized (mDataStore) {
try {
- if (!mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
- // No need to show the notification only if layout selection didn't change
+ if (mDataStore.setSelectedKeyboardLayouts(key, selectedLayouts)) {
+ // Need to show the notification only if layout selection changed
// from the previous configuration
- return;
+ needToShowNotification = true;
}
} finally {
mDataStore.saveIfNeeded();
}
}
}
- maybeUpdateNotification();
+ if (needToShowNotification) {
+ maybeUpdateNotification();
+ }
}
private String getDefaultKeyboardLayout(final InputDevice inputDevice) {
@@ -323,12 +345,14 @@
reloadKeyboardLayouts();
}
+ @AnyThread
public KeyboardLayout[] getKeyboardLayouts() {
final ArrayList<KeyboardLayout> list = new ArrayList<>();
visitAllKeyboardLayouts((resources, keyboardLayoutResId, layout) -> list.add(layout));
return list.toArray(new KeyboardLayout[0]);
}
+ @AnyThread
public KeyboardLayout[] getKeyboardLayoutsForInputDevice(
final InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
@@ -375,6 +399,7 @@
KeyboardLayout[]::new);
}
+ @AnyThread
@Nullable
public KeyboardLayout getKeyboardLayout(String keyboardLayoutDescriptor) {
Objects.requireNonNull(keyboardLayoutDescriptor,
@@ -543,6 +568,7 @@
return key.toString();
}
+ @AnyThread
@Nullable
public String getCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
@@ -566,6 +592,7 @@
}
}
+ @AnyThread
public void setCurrentKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
@@ -592,6 +619,7 @@
}
}
+ @AnyThread
public String[] getEnabledKeyboardLayoutsForInputDevice(InputDeviceIdentifier identifier) {
if (useNewSettingsUi()) {
Slog.e(TAG, "getEnabledKeyboardLayoutsForInputDevice API not supported");
@@ -608,6 +636,7 @@
}
}
+ @AnyThread
public void addKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
@@ -635,6 +664,7 @@
}
}
+ @AnyThread
public void removeKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
String keyboardLayoutDescriptor) {
if (useNewSettingsUi()) {
@@ -667,6 +697,7 @@
}
}
+ @AnyThread
public void switchKeyboardLayout(int deviceId, int direction) {
if (useNewSettingsUi()) {
Slog.e(TAG, "switchKeyboardLayout API not supported");
@@ -675,7 +706,7 @@
mHandler.obtainMessage(MSG_SWITCH_KEYBOARD_LAYOUT, deviceId, direction).sendToTarget();
}
- // Must be called on handler.
+ @MainThread
private void handleSwitchKeyboardLayout(int deviceId, int direction) {
final InputDevice device = getInputDevice(deviceId);
if (device != null) {
@@ -713,23 +744,14 @@
}
@Nullable
+ @AnyThread
public String[] getKeyboardLayoutOverlay(InputDeviceIdentifier identifier) {
String keyboardLayoutDescriptor;
if (useNewSettingsUi()) {
- InputDevice inputDevice = getInputDevice(identifier);
- if (inputDevice == null) {
- // getKeyboardLayoutOverlay() called before input device added completely. Need
- // to wait till the device is added which will call reloadKeyboardLayouts()
- return null;
+ synchronized (mImeInfoLock) {
+ keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
+ mCurrentImeInfo);
}
- if (mCurrentImeInfo == null) {
- // Haven't received onInputMethodSubtypeChanged() callback from IMMS. Will reload
- // keyboard layouts once we receive the callback.
- return null;
- }
-
- keyboardLayoutDescriptor = getKeyboardLayoutForInputDeviceInternal(identifier,
- mCurrentImeInfo);
} else {
keyboardLayoutDescriptor = getCurrentKeyboardLayoutForInputDevice(identifier);
}
@@ -755,6 +777,7 @@
return result;
}
+ @AnyThread
@Nullable
public String getKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@@ -773,6 +796,7 @@
return layout;
}
+ @AnyThread
public void setKeyboardLayoutForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype,
@@ -783,8 +807,8 @@
}
Objects.requireNonNull(keyboardLayoutDescriptor,
"keyboardLayoutDescriptor must not be null");
- String key = createLayoutKey(identifier, userId,
- InputMethodSubtypeHandle.of(imeInfo, imeSubtype));
+ String key = createLayoutKey(identifier,
+ new ImeInfo(userId, InputMethodSubtypeHandle.of(imeInfo, imeSubtype), imeSubtype));
synchronized (mDataStore) {
try {
// Key for storing into data store = <device descriptor>,<userId>,<subtypeHandle>
@@ -803,6 +827,7 @@
}
}
+ @AnyThread
public KeyboardLayout[] getKeyboardLayoutListForInputDevice(InputDeviceIdentifier identifier,
@UserIdInt int userId, @NonNull InputMethodInfo imeInfo,
@Nullable InputMethodSubtype imeSubtype) {
@@ -815,8 +840,8 @@
}
private KeyboardLayout[] getKeyboardLayoutListForInputDeviceInternal(
- InputDeviceIdentifier identifier, ImeInfo imeInfo) {
- String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+ InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) {
+ String key = createLayoutKey(identifier, imeInfo);
// Fetch user selected layout and always include it in layout list.
String userSelectedLayout;
@@ -826,7 +851,7 @@
final ArrayList<KeyboardLayout> potentialLayouts = new ArrayList<>();
String imeLanguageTag;
- if (imeInfo.mImeSubtype == null) {
+ if (imeInfo == null || imeInfo.mImeSubtype == null) {
imeLanguageTag = "";
} else {
ULocale imeLocale = imeInfo.mImeSubtype.getPhysicalKeyboardHintLanguageTag();
@@ -866,6 +891,7 @@
return potentialLayouts.toArray(new KeyboardLayout[0]);
}
+ @AnyThread
public void onInputMethodSubtypeChanged(@UserIdInt int userId,
@Nullable InputMethodSubtypeHandle subtypeHandle,
@Nullable InputMethodSubtype subtype) {
@@ -879,25 +905,45 @@
}
return;
}
- if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
- || mCurrentImeInfo.mUserId != userId) {
- mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
- mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
- if (DEBUG) {
- Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
- + " subtypeHandle=" + subtypeHandle);
+ synchronized (mImeInfoLock) {
+ if (mCurrentImeInfo == null || !subtypeHandle.equals(mCurrentImeInfo.mImeSubtypeHandle)
+ || mCurrentImeInfo.mUserId != userId) {
+ mCurrentImeInfo = new ImeInfo(userId, subtypeHandle, subtype);
+ mHandler.sendEmptyMessage(MSG_CURRENT_IME_INFO_CHANGED);
+ if (DEBUG) {
+ Slog.d(TAG, "InputMethodSubtype changed: userId=" + userId
+ + " subtypeHandle=" + subtypeHandle);
+ }
+ }
+ }
+ }
+
+ @MainThread
+ private void onCurrentImeInfoChanged() {
+ synchronized (mImeInfoLock) {
+ for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
+ InputDevice inputDevice = Objects.requireNonNull(
+ getInputDevice(mConfiguredKeyboards.keyAt(i)));
+ String layout = getKeyboardLayoutForInputDeviceInternal(inputDevice.getIdentifier(),
+ mCurrentImeInfo);
+ KeyboardConfiguration config = mConfiguredKeyboards.valueAt(i);
+ if (!Objects.equals(layout, config.getCurrentLayout())) {
+ config.setCurrentLayout(layout);
+ mHandler.sendEmptyMessage(MSG_RELOAD_KEYBOARD_LAYOUTS);
+ return;
+ }
}
}
}
@Nullable
private String getKeyboardLayoutForInputDeviceInternal(InputDeviceIdentifier identifier,
- ImeInfo imeInfo) {
+ @Nullable ImeInfo imeInfo) {
InputDevice inputDevice = getInputDevice(identifier);
if (inputDevice == null || inputDevice.isVirtual() || !inputDevice.isFullKeyboard()) {
return null;
}
- String key = createLayoutKey(identifier, imeInfo.mUserId, imeInfo.mImeSubtypeHandle);
+ String key = createLayoutKey(identifier, imeInfo);
String layout;
synchronized (mDataStore) {
layout = mDataStore.getKeyboardLayout(getLayoutDescriptor(identifier), key);
@@ -923,11 +969,7 @@
@Nullable
private static String getDefaultKeyboardLayoutBasedOnImeInfo(InputDevice inputDevice,
- ImeInfo imeInfo, KeyboardLayout[] layoutList) {
- if (imeInfo.mImeSubtypeHandle == null) {
- return null;
- }
-
+ @Nullable ImeInfo imeInfo, KeyboardLayout[] layoutList) {
Arrays.sort(layoutList);
// Check <VendorID, ProductID> matching for explicitly declared custom KCM files.
@@ -961,12 +1003,12 @@
}
}
- InputMethodSubtype subtype = imeInfo.mImeSubtype;
- // Can't auto select layout based on IME if subtype or language tag is null
- if (subtype == null) {
+ if (imeInfo == null || imeInfo.mImeSubtypeHandle == null || imeInfo.mImeSubtype == null) {
+ // Can't auto select layout based on IME info is null
return null;
}
+ InputMethodSubtype subtype = imeInfo.mImeSubtype;
// Check layout type, language tag information from IME for matching
ULocale pkLocale = subtype.getPhysicalKeyboardHintLanguageTag();
String pkLanguageTag =
@@ -1043,6 +1085,7 @@
mNative.reloadKeyboardLayouts();
}
+ @MainThread
private void maybeUpdateNotification() {
if (mConfiguredKeyboards.size() == 0) {
hideKeyboardLayoutNotification();
@@ -1051,7 +1094,7 @@
for (int i = 0; i < mConfiguredKeyboards.size(); i++) {
// If we have a keyboard with no selected layouts, we should always show missing
// layout notification even if there are other keyboards that are configured properly.
- if (mConfiguredKeyboards.valueAt(i).isEmpty()) {
+ if (!mConfiguredKeyboards.valueAt(i).hasConfiguredLayouts()) {
showMissingKeyboardLayoutNotification();
return;
}
@@ -1059,7 +1102,7 @@
showConfiguredKeyboardLayoutNotification();
}
- // Must be called on handler.
+ @MainThread
private void showMissingKeyboardLayoutNotification() {
final Resources r = mContext.getResources();
final String missingKeyboardLayoutNotificationContent = r.getString(
@@ -1084,6 +1127,7 @@
}
}
+ @MainThread
private void showKeyboardLayoutNotification(@NonNull String intentTitle,
@NonNull String intentContent, @Nullable InputDevice targetDevice) {
final NotificationManager notificationManager = mContext.getSystemService(
@@ -1119,7 +1163,7 @@
notification, UserHandle.ALL);
}
- // Must be called on handler.
+ @MainThread
private void hideKeyboardLayoutNotification() {
NotificationManager notificationManager = mContext.getSystemService(
NotificationManager.class);
@@ -1132,6 +1176,7 @@
UserHandle.ALL);
}
+ @MainThread
private void showConfiguredKeyboardLayoutNotification() {
final Resources r = mContext.getResources();
@@ -1144,8 +1189,8 @@
}
final InputDevice inputDevice = getInputDevice(mConfiguredKeyboards.keyAt(0));
- final Set<String> selectedLayouts = mConfiguredKeyboards.valueAt(0);
- if (inputDevice == null || selectedLayouts == null || selectedLayouts.isEmpty()) {
+ final KeyboardConfiguration config = mConfiguredKeyboards.valueAt(0);
+ if (inputDevice == null || !config.hasConfiguredLayouts()) {
return;
}
@@ -1153,10 +1198,11 @@
r.getString(
R.string.keyboard_layout_notification_selected_title,
inputDevice.getName()),
- createConfiguredNotificationText(mContext, selectedLayouts),
+ createConfiguredNotificationText(mContext, config.getConfiguredLayouts()),
inputDevice);
}
+ @MainThread
private String createConfiguredNotificationText(@NonNull Context context,
@NonNull Set<String> selectedLayouts) {
final Resources r = context.getResources();
@@ -1199,6 +1245,9 @@
case MSG_UPDATE_KEYBOARD_LAYOUTS:
updateKeyboardLayouts();
return true;
+ case MSG_CURRENT_IME_INFO_CHANGED:
+ onCurrentImeInfoChanged();
+ return true;
default:
return false;
}
@@ -1252,17 +1301,19 @@
return imeInfoList;
}
- private String createLayoutKey(InputDeviceIdentifier identifier, int userId,
- @NonNull InputMethodSubtypeHandle subtypeHandle) {
- Objects.requireNonNull(subtypeHandle, "subtypeHandle must not be null");
- return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + userId
- + ",subtypeHandle:" + subtypeHandle.toStringHandle();
+ private String createLayoutKey(InputDeviceIdentifier identifier, @Nullable ImeInfo imeInfo) {
+ if (imeInfo == null) {
+ return getLayoutDescriptor(identifier);
+ }
+ Objects.requireNonNull(imeInfo.mImeSubtypeHandle, "subtypeHandle must not be null");
+ return "layoutDescriptor:" + getLayoutDescriptor(identifier) + ",userId:" + imeInfo.mUserId
+ + ",subtypeHandle:" + imeInfo.mImeSubtypeHandle.toStringHandle();
}
private static boolean isLayoutCompatibleWithLanguageTag(KeyboardLayout layout,
@NonNull String languageTag) {
LocaleList layoutLocales = layout.getLocales();
- if (layoutLocales.isEmpty()) {
+ if (layoutLocales.isEmpty() || TextUtils.isEmpty(languageTag)) {
// KCM file doesn't have an associated language tag. This can be from
// a 3rd party app so need to include it as a potential layout.
return true;
@@ -1350,6 +1401,39 @@
}
}
+ private static class KeyboardConfiguration {
+ // If null or empty, it means no layout is configured for the device. And user needs to
+ // manually set up the device.
+ @Nullable
+ private Set<String> mConfiguredLayouts;
+
+ // If null, it means no layout is selected for the device.
+ @Nullable
+ private String mCurrentLayout;
+
+ private boolean hasConfiguredLayouts() {
+ return mConfiguredLayouts != null && !mConfiguredLayouts.isEmpty();
+ }
+
+ @Nullable
+ private Set<String> getConfiguredLayouts() {
+ return mConfiguredLayouts;
+ }
+
+ private void setConfiguredLayouts(Set<String> configuredLayouts) {
+ mConfiguredLayouts = configuredLayouts;
+ }
+
+ @Nullable
+ private String getCurrentLayout() {
+ return mCurrentLayout;
+ }
+
+ private void setCurrentLayout(String currentLayout) {
+ mCurrentLayout = currentLayout;
+ }
+ }
+
private interface KeyboardLayoutVisitor {
void visitKeyboardLayout(Resources resources,
int keyboardLayoutResId, KeyboardLayout layout);
diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
index 94d5aab..7a51126 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -19,9 +19,19 @@
import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_CREATED;
import static android.app.ActivityManagerInternal.MEDIA_PROJECTION_TOKEN_EVENT_DESTROYED;
+import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.media.projection.IMediaProjectionManager.EXTRA_PACKAGE_REUSING_GRANTED_CONSENT;
+import static android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTED_CONSENT;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK;
+import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN;
+import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
import android.Manifest;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManagerInternal;
@@ -30,7 +40,9 @@
import android.app.compat.CompatChanges;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
+import android.content.ComponentName;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -45,11 +57,13 @@
import android.media.projection.IMediaProjectionWatcherCallback;
import android.media.projection.MediaProjectionInfo;
import android.media.projection.MediaProjectionManager;
+import android.media.projection.ReviewGrantedConsentResult;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
+import android.os.PermissionEnforcer;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
@@ -57,6 +71,7 @@
import android.util.Slog;
import android.view.ContentRecordingSession;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -69,6 +84,7 @@
import java.io.PrintWriter;
import java.time.Duration;
import java.util.Map;
+import java.util.Objects;
/**
* Manages MediaProjection sessions.
@@ -161,10 +177,9 @@
}
}
-
@Override
public void onStart() {
- publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(),
+ publishBinderService(Context.MEDIA_PROJECTION_SERVICE, new BinderService(mContext),
false /*allowIsolated*/);
mMediaRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mMediaRouterCallback,
MediaRouter.CALLBACK_FLAG_PASSIVE_DISCOVERY);
@@ -305,6 +320,10 @@
}
return false;
}
+ if (mProjectionGrant != null) {
+ // Cache the session details.
+ mProjectionGrant.mSession = incomingSession;
+ }
return true;
}
}
@@ -323,9 +342,8 @@
}
}
-
/**
- * Reshows the permisison dialog for the user to review consent they've already granted in
+ * Re-shows the permission dialog for the user to review consent they've already granted in
* the given projection instance.
*
* <p>Preconditions:
@@ -337,18 +355,111 @@
* <p>Returns immediately but waits to start recording until user has reviewed their consent.
*/
@VisibleForTesting
- void requestConsentForInvalidProjection(IMediaProjection projection) {
+ void requestConsentForInvalidProjection() {
synchronized (mLock) {
Slog.v(TAG, "Reusing token: Reshow dialog for due to invalid projection.");
- // TODO(b/274790702): Trigger the permission dialog again in SysUI.
+ // Trigger the permission dialog again in SysUI
+ // Do not handle the result; SysUI will update us when the user has consented.
+ mContext.startActivityAsUser(buildReviewGrantedConsentIntent(),
+ UserHandle.getUserHandleForUid(mProjectionGrant.uid));
+ }
+ }
+
+ /**
+ * Returns an intent to re-show the consent dialog in SysUI. Should only be used for the
+ * scenario where the host app has re-used the consent token.
+ *
+ * <p>Consent dialog result handled in
+ * {@link BinderService#setUserReviewGrantedConsentResult(int)}.
+ */
+ private Intent buildReviewGrantedConsentIntent() {
+ final String permissionDialogString = mContext.getResources().getString(
+ R.string.config_mediaProjectionPermissionDialogComponent);
+ final ComponentName mediaProjectionPermissionDialogComponent =
+ ComponentName.unflattenFromString(permissionDialogString);
+ // We can use mProjectionGrant since we already checked that it matches the given token.
+ return new Intent().setComponent(mediaProjectionPermissionDialogComponent)
+ .putExtra(EXTRA_USER_REVIEW_GRANTED_CONSENT, true)
+ .putExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT, mProjectionGrant.packageName)
+ .setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ }
+
+ /**
+ * Handles result of dialog shown from {@link BinderService#buildReviewGrantedConsentIntent()}.
+ *
+ * <p>Tears down session if user did not consent, or starts mirroring if user did consent.
+ */
+ @VisibleForTesting
+ void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult,
+ @Nullable IMediaProjection projection) {
+ synchronized (mLock) {
+ final boolean consentGranted =
+ consentResult == RECORD_CONTENT_DISPLAY || consentResult == RECORD_CONTENT_TASK;
+ if (consentGranted && projection == null || !isCurrentProjection(
+ projection.asBinder())) {
+ Slog.v(TAG, "Reusing token: Ignore consent result of " + consentResult + " for a "
+ + "token that isn't current");
+ return;
+ }
+ if (mProjectionGrant == null) {
+ Slog.w(TAG, "Reusing token: Can't review consent with no ongoing projection.");
+ return;
+ }
+ if (mProjectionGrant.mSession == null
+ || !mProjectionGrant.mSession.isWaitingToRecord()) {
+ Slog.w(TAG, "Reusing token: Ignore consent result " + consentResult
+ + " if not waiting for the result.");
+ return;
+ }
+ Slog.v(TAG, "Reusing token: Handling user consent result " + consentResult);
+ switch (consentResult) {
+ case UNKNOWN:
+ case RECORD_CANCEL:
+ // Pass in null to stop mirroring.
+ setReviewedConsentSessionLocked(/* session= */ null);
+ // The grant may now be null if setting the session failed.
+ if (mProjectionGrant != null) {
+ // Always stop the projection.
+ mProjectionGrant.stop();
+ }
+ break;
+ case RECORD_CONTENT_DISPLAY:
+ // TODO(270118861) The app may have specified a particular id in the virtual
+ // display config. However - below will always return INVALID since it checks
+ // that window manager mirroring is not enabled (it is always enabled for MP).
+ setReviewedConsentSessionLocked(ContentRecordingSession.createDisplaySession(
+ DEFAULT_DISPLAY));
+ break;
+ case RECORD_CONTENT_TASK:
+ setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession(
+ mProjectionGrant.getLaunchCookie()));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the session after the user has reviewed consent. There must be a current session.
+ *
+ * @param session The new session details, or {@code null} to stop recording.
+ */
+ private void setReviewedConsentSessionLocked(@Nullable ContentRecordingSession session) {
+ if (session != null) {
+ session.setWaitingToRecord(false);
+ session.setVirtualDisplayId(mProjectionGrant.mVirtualDisplayId);
+ }
+
+ Slog.v(TAG, "Reusing token: Processed consent so set the session " + session);
+ if (!setContentRecordingSession(session)) {
+ Slog.e(TAG, "Reusing token: Failed to set session for reused consent, so stop");
+ // Do not need to invoke stop; updating the session does it for us.
}
}
// TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere.
@VisibleForTesting
MediaProjection createProjectionInternal(int uid, String packageName, int type,
- boolean isPermanentGrant, UserHandle callingUser,
- boolean packageAttemptedReusingGrantedConsent) {
+ boolean isPermanentGrant, UserHandle callingUser) {
MediaProjection projection;
ApplicationInfo ai;
try {
@@ -371,6 +482,34 @@
return projection;
}
+ // TODO(b/261563516): Remove internal method and test aidl directly, here and elsewhere.
+ @VisibleForTesting
+ MediaProjection getProjectionInternal(int uid, String packageName) {
+ final long callingToken = Binder.clearCallingIdentity();
+ try {
+ // Supposedly the package has re-used the user's consent; confirm the provided details
+ // against the current projection token before re-using the current projection.
+ if (mProjectionGrant == null || mProjectionGrant.mSession == null
+ || !mProjectionGrant.mSession.isWaitingToRecord()) {
+ Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ + "instance");
+ return null;
+ }
+ // The package matches, go ahead and re-use the token for this request.
+ if (mProjectionGrant.uid == uid
+ && Objects.equals(mProjectionGrant.packageName, packageName)) {
+ Slog.v(TAG, "Reusing token: getProjection can reuse the current projection");
+ return mProjectionGrant;
+ } else {
+ Slog.e(TAG, "Reusing token: Not possible to reuse the current projection "
+ + "instance due to package details mismatching");
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingToken);
+ }
+ }
+
@VisibleForTesting
MediaProjectionInfo getActiveProjectionInfo() {
synchronized (mLock) {
@@ -395,6 +534,10 @@
private final class BinderService extends IMediaProjectionManager.Stub {
+ BinderService(Context context) {
+ super(PermissionEnforcer.fromContext(context));
+ }
+
@Override // Binder call
public boolean hasProjectionPermission(int uid, String packageName) {
final long token = Binder.clearCallingIdentity();
@@ -424,7 +567,25 @@
}
final UserHandle callingUser = Binder.getCallingUserHandle();
return createProjectionInternal(uid, packageName, type, isPermanentGrant,
- callingUser, false);
+ callingUser);
+ }
+
+ @Override // Binder call
+ @EnforcePermission(MANAGE_MEDIA_PROJECTION)
+ public IMediaProjection getProjection(int uid, String packageName) {
+ getProjection_enforcePermission();
+ if (packageName == null || packageName.isEmpty()) {
+ throw new IllegalArgumentException("package name must not be empty");
+ }
+
+ MediaProjection projection;
+ final long callingToken = Binder.clearCallingIdentity();
+ try {
+ projection = getProjectionInternal(uid, packageName);
+ } finally {
+ Binder.restoreCallingIdentity(callingToken);
+ }
+ return projection;
}
@Override // Binder call
@@ -562,7 +723,7 @@
}
@Override
- public void requestConsentForInvalidProjection(IMediaProjection projection) {
+ public void requestConsentForInvalidProjection(@NonNull IMediaProjection projection) {
if (mContext.checkCallingOrSelfPermission(Manifest.permission.MANAGE_MEDIA_PROJECTION)
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Requires MANAGE_MEDIA_PROJECTION to check if the given"
@@ -577,7 +738,22 @@
// Remove calling app identity before performing any privileged operations.
final long token = Binder.clearCallingIdentity();
try {
- MediaProjectionManagerService.this.requestConsentForInvalidProjection(projection);
+ MediaProjectionManagerService.this.requestConsentForInvalidProjection();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override // Binder call
+ @EnforcePermission(MANAGE_MEDIA_PROJECTION)
+ public void setUserReviewGrantedConsentResult(@ReviewGrantedConsentResult int consentResult,
+ @Nullable IMediaProjection projection) {
+ setUserReviewGrantedConsentResult_enforcePermission();
+ // Remove calling app identity before performing any privileged operations.
+ final long token = Binder.clearCallingIdentity();
+ try {
+ MediaProjectionManagerService.this.setUserReviewGrantedConsentResult(consentResult,
+ projection);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -594,7 +770,6 @@
}
}
-
private boolean checkPermission(String packageName, String permission) {
return mContext.getPackageManager().checkPermission(permission, packageName)
== PackageManager.PERMISSION_GRANTED;
@@ -630,6 +805,8 @@
// Set if MediaProjection#createVirtualDisplay has been invoked previously (it
// should only be called once).
private int mVirtualDisplayId = INVALID_DISPLAY;
+ // The associated session details already sent to WindowManager.
+ private ContentRecordingSession mSession;
MediaProjection(int type, int uid, String packageName, int targetSdkVersion,
boolean isPrivileged) {
@@ -883,6 +1060,18 @@
}
synchronized (mLock) {
mVirtualDisplayId = displayId;
+
+ // If prior session was does not have a valid display id, then update the display
+ // so recording can start.
+ if (mSession != null && mSession.getVirtualDisplayId() == INVALID_DISPLAY) {
+ Slog.v(TAG, "Virtual display now created, so update session with the virtual "
+ + "display id");
+ mSession.setVirtualDisplayId(mVirtualDisplayId);
+ if (!setContentRecordingSession(mSession)) {
+ Slog.e(TAG, "Failed to set session for virtual display id");
+ // Do not need to invoke stop; updating the session does it for us.
+ }
+ }
}
}
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 0532a79..247a5c0 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -2987,6 +2987,7 @@
}
break;
case KeyEvent.KEYCODE_H:
+ case KeyEvent.KEYCODE_ENTER:
if (event.isMetaPressed()) {
return handleHomeShortcuts(displayId, focusedToken, event);
}
diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java
index 8bbcd27..c40d72c 100644
--- a/services/core/java/com/android/server/wm/ActivityClientController.java
+++ b/services/core/java/com/android/server/wm/ActivityClientController.java
@@ -807,11 +807,8 @@
if (under != null) {
under.returningOptions = safeOptions != null ? safeOptions.getOptions(r) : null;
}
- // Create a transition if the activity is playing in case the current activity
- // didn't commit invisible. That's because if this activity has changed its
- // visibility while playing transition, there won't able to commit visibility until
- // the running transition finish.
- final Transition transition = r.mTransitionController.inPlayingTransition(r)
+ // Create a transition to make sure the activity change is collected.
+ final Transition transition = r.mTransitionController.isShellTransitionsEnabled()
&& !r.mTransitionController.isCollecting()
? r.mTransitionController.createTransition(TRANSIT_TO_FRONT) : null;
final boolean changed = r.setOccludesParent(false);
diff --git a/services/core/java/com/android/server/wm/ContentRecordingController.java b/services/core/java/com/android/server/wm/ContentRecordingController.java
index a41dcc6..040da88 100644
--- a/services/core/java/com/android/server/wm/ContentRecordingController.java
+++ b/services/core/java/com/android/server/wm/ContentRecordingController.java
@@ -80,7 +80,7 @@
}
// Invalid scenario: ignore identical incoming session.
if (ContentRecordingSession.isProjectionOnSameDisplay(mSession, incomingSession)) {
- // TODO(242833866) if incoming session is no longer waiting to record, allow
+ // TODO(242833866): if incoming session is no longer waiting to record, allow
// the update through.
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING,
@@ -99,7 +99,7 @@
incomingDisplayContent = wmService.mRoot.getDisplayContentOrCreate(
incomingSession.getVirtualDisplayId());
incomingDisplayContent.setContentRecordingSession(incomingSession);
- // TODO(b/270118861) When user grants consent to re-use, explicitly ask ContentRecorder
+ // TODO(b/270118861): When user grants consent to re-use, explicitly ask ContentRecorder
// to update, since no config/display change arrives. Mark recording as black.
}
// Takeover and stopping scenario: stop recording on the pre-existing session.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index f478e9b..8bfa426 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2267,6 +2267,12 @@
if (cutout == null || cutout == DisplayCutout.NO_CUTOUT) {
return WmDisplayCutout.NO_CUTOUT;
}
+ if (displayWidth == displayHeight) {
+ Slog.w(TAG, "Ignore cutout because display size is square: " + displayWidth);
+ // Avoid UnsupportedOperationException because DisplayCutout#computeSafeInsets doesn't
+ // support square size.
+ return WmDisplayCutout.NO_CUTOUT;
+ }
if (rotation == ROTATION_0) {
return WmDisplayCutout.computeSafeInsets(
cutout, displayWidth, displayHeight);
@@ -3087,13 +3093,9 @@
mIsSizeForced = mInitialDisplayWidth != width || mInitialDisplayHeight != height;
if (mIsSizeForced) {
- // Set some sort of reasonable bounds on the size of the display that we will try
- // to emulate.
- final int minSize = 200;
- final int maxScale = 3;
- final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale;
- width = Math.min(Math.max(width, minSize), maxSize);
- height = Math.min(Math.max(height, minSize), maxSize);
+ final Point size = getValidForcedSize(width, height);
+ width = size.x;
+ height = size.y;
}
Slog.i(TAG_WM, "Using new display size: " + width + "x" + height);
@@ -3108,6 +3110,16 @@
mWmService.mDisplayWindowSettings.setForcedSize(this, width, height);
}
+ /** Returns a reasonable size for setting forced display size. */
+ Point getValidForcedSize(int w, int h) {
+ final int minSize = 200;
+ final int maxScale = 3;
+ final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale;
+ w = Math.min(Math.max(w, minSize), maxSize);
+ h = Math.min(Math.max(h, minSize), maxSize);
+ return new Point(w, h);
+ }
+
DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) {
if (mDisplayPolicy == null || mInitialDisplayCutout == null) {
return null;
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 8822193..40b8274 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -5780,10 +5780,12 @@
if (sizeStr != null && sizeStr.length() > 0) {
final int pos = sizeStr.indexOf(',');
if (pos > 0 && sizeStr.lastIndexOf(',') == pos) {
- int width, height;
try {
- width = Integer.parseInt(sizeStr.substring(0, pos));
- height = Integer.parseInt(sizeStr.substring(pos + 1));
+ final Point size = displayContent.getValidForcedSize(
+ Integer.parseInt(sizeStr.substring(0, pos)),
+ Integer.parseInt(sizeStr.substring(pos + 1)));
+ final int width = size.x;
+ final int height = size.y;
if (displayContent.mBaseDisplayWidth != width
|| displayContent.mBaseDisplayHeight != height) {
ProtoLog.i(WM_ERROR, "FORCED DISPLAY SIZE: %dx%d", width, height);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index f6bc93a..ee75f84 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -14744,24 +14744,24 @@
synchronized (getLockObject()) {
enforcingAdmin = enforceCanCallLockTaskLocked(who, caller.getPackageName());
}
- if (packages.length == 0) {
+ LockTaskPolicy currentPolicy = mDevicePolicyEngine.getLocalPolicySetByAdmin(
+ PolicyDefinition.LOCK_TASK,
+ enforcingAdmin,
+ caller.getUserId());
+ LockTaskPolicy policy;
+ if (currentPolicy == null) {
+ policy = new LockTaskPolicy(Set.of(packages));
+ } else {
+ policy = new LockTaskPolicy(currentPolicy);
+ policy.setPackages(Set.of(packages));
+ }
+ if (policy.getPackages().isEmpty()
+ && policy.getFlags() == DevicePolicyManager.LOCK_TASK_FEATURE_NONE) {
mDevicePolicyEngine.removeLocalPolicy(
PolicyDefinition.LOCK_TASK,
enforcingAdmin,
caller.getUserId());
} else {
- LockTaskPolicy currentPolicy = mDevicePolicyEngine.getLocalPolicySetByAdmin(
- PolicyDefinition.LOCK_TASK,
- enforcingAdmin,
- caller.getUserId());
- LockTaskPolicy policy;
- if (currentPolicy == null) {
- policy = new LockTaskPolicy(Set.of(packages));
- } else {
- policy = new LockTaskPolicy(currentPolicy);
- policy.setPackages(Set.of(packages));
- }
-
mDevicePolicyEngine.setLocalPolicy(
PolicyDefinition.LOCK_TASK,
enforcingAdmin,
@@ -14876,18 +14876,26 @@
PolicyDefinition.LOCK_TASK,
enforcingAdmin,
caller.getUserId());
+ LockTaskPolicy policy;
if (currentPolicy == null) {
- throw new IllegalArgumentException("Can't set a lock task flags without setting "
- + "lock task packages first.");
+ policy = new LockTaskPolicy(flags);
+ } else {
+ policy = new LockTaskPolicy(currentPolicy);
+ policy.setFlags(flags);
}
- LockTaskPolicy policy = new LockTaskPolicy(currentPolicy);
- policy.setFlags(flags);
-
- mDevicePolicyEngine.setLocalPolicy(
- PolicyDefinition.LOCK_TASK,
- enforcingAdmin,
- policy,
- caller.getUserId());
+ if (policy.getPackages().isEmpty()
+ && policy.getFlags() == DevicePolicyManager.LOCK_TASK_FEATURE_NONE) {
+ mDevicePolicyEngine.removeLocalPolicy(
+ PolicyDefinition.LOCK_TASK,
+ enforcingAdmin,
+ caller.getUserId());
+ } else {
+ mDevicePolicyEngine.setLocalPolicy(
+ PolicyDefinition.LOCK_TASK,
+ enforcingAdmin,
+ policy,
+ caller.getUserId());
+ }
} else {
Objects.requireNonNull(who, "ComponentName is null");
synchronized (getLockObject()) {
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
index 0f6f3c5..20bd2d7 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java
@@ -42,10 +42,6 @@
void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer,
@NonNull LockTaskPolicy value) throws IOException {
Objects.requireNonNull(value);
- if (value.getPackages() == null || value.getPackages().isEmpty()) {
- throw new IllegalArgumentException("Error saving LockTaskPolicy to file, lock task "
- + "packages must be present");
- }
serializer.attribute(
/* namespace= */ null,
ATTR_PACKAGES,
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
index 0f4d4e8..64c05dc 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardBacklightControllerTests.kt
@@ -250,20 +250,24 @@
`when`(iInputManager.getInputDevice(DEVICE_ID)).thenReturn(keyboardWithBacklight)
`when`(iInputManager.getLights(DEVICE_ID)).thenReturn(listOf(keyboardBacklight))
- dataStore.setKeyboardBacklightBrightness(
- keyboardWithBacklight.descriptor,
- LIGHT_ID,
- MAX_BRIGHTNESS
- )
+ for (level in 1 until BRIGHTNESS_VALUE_FOR_LEVEL.size) {
+ dataStore.setKeyboardBacklightBrightness(
+ keyboardWithBacklight.descriptor,
+ LIGHT_ID,
+ BRIGHTNESS_VALUE_FOR_LEVEL[level] - 1
+ )
- keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
- keyboardBacklightController.notifyUserActivity()
- testLooper.dispatchNext()
- assertEquals(
- "Keyboard backlight level should be restored to the level saved in the data store",
- Color.argb(MAX_BRIGHTNESS, 0, 0, 0),
- lightColorMap[LIGHT_ID]
- )
+ keyboardBacklightController.onInputDeviceAdded(DEVICE_ID)
+ keyboardBacklightController.notifyUserActivity()
+ testLooper.dispatchNext()
+ assertEquals(
+ "Keyboard backlight level should be restored to the level saved in the data " +
+ "store",
+ Color.argb(BRIGHTNESS_VALUE_FOR_LEVEL[level], 0, 0, 0),
+ lightColorMap[LIGHT_ID]
+ )
+ keyboardBacklightController.onInputDeviceRemoved(DEVICE_ID)
+ }
}
@Test
diff --git a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
index ea3f3bc..d0d28c3 100644
--- a/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
+++ b/services/tests/servicestests/src/com/android/server/input/KeyboardLayoutManagerTests.kt
@@ -633,6 +633,30 @@
0,
keyboardLayouts.size
)
+
+ // If IME doesn't have a corresponding language tag, then should show all available
+ // layouts no matter the script code.
+ keyboardLayouts =
+ keyboardLayoutManager.getKeyboardLayoutListForInputDevice(
+ keyboardDevice.identifier, USER_ID, imeInfo, null
+ )
+ assertNotEquals(
+ "New UI: getKeyboardLayoutListForInputDevice API should return all layouts if" +
+ "language tag or subtype not provided",
+ 0,
+ keyboardLayouts.size
+ )
+ assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Latin " +
+ "layouts if language tag or subtype not provided",
+ containsLayout(keyboardLayouts, ENGLISH_US_LAYOUT_DESCRIPTOR)
+ )
+ assertTrue("New UI: getKeyboardLayoutListForInputDevice API should contain Cyrillic " +
+ "layouts if language tag or subtype not provided",
+ containsLayout(
+ keyboardLayouts,
+ createLayoutDescriptor("keyboard_layout_russian")
+ )
+ )
}
}
diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
index 36c2001..5751db0 100644
--- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java
@@ -19,15 +19,25 @@
import static android.content.pm.ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
import static android.media.projection.MediaProjectionManager.TYPE_MIRRORING;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_DISPLAY;
+import static android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK;
+import static android.media.projection.ReviewGrantedConsentResult.UNKNOWN;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertThrows;
import android.app.ActivityManagerInternal;
@@ -37,7 +47,9 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.ApplicationInfoFlags;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.projection.IMediaProjection;
import android.media.projection.IMediaProjectionCallback;
+import android.media.projection.ReviewGrantedConsentResult;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -56,6 +68,8 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -108,6 +122,8 @@
private WindowManagerInternal mWindowManagerInternal;
@Mock
private PackageManager mPackageManager;
+ @Captor
+ private ArgumentCaptor<ContentRecordingSession> mSessionCaptor;
@Before
public void setup() throws Exception {
@@ -154,12 +170,15 @@
@Test
public void testCreateProjection() throws NameNotFoundException {
- MediaProjectionManagerService.MediaProjection projection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ // Create a first projection.
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
+ // We are allowed to create a new projection.
MediaProjectionManagerService.MediaProjection secondProjection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ startProjectionPreconditions();
+
+ // This is a new projection.
assertThat(secondProjection).isNotNull();
assertThat(secondProjection).isNotEqualTo(projection);
}
@@ -167,44 +186,58 @@
@Test
public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
throws NameNotFoundException {
- MediaProjectionManagerService.MediaProjection projection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ // Create a first projection.
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
- MediaProjectionManagerService.MediaProjection secondProjection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
-
- assertThat(secondProjection).isNotNull();
- assertThat(secondProjection).isNotEqualTo(projection);
+ // We are not allowed to retrieve the prior projection, since we are not waiting for the
+ // user's consent.
+ assertThat(startReusedProjectionPreconditions()).isNull();
}
@Test
public void testCreateProjection_attemptReuse_priorProjectionGrant_notWaiting()
throws NameNotFoundException {
- MediaProjectionManagerService.MediaProjection projection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ // Create a first projection.
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
- // Mark this projection as not waiting.
+ // Mark this projection as not waiting for the user to review consent.
doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
any(ContentRecordingSession.class));
mService.setContentRecordingSession(DISPLAY_SESSION);
- // We are allowed to create another projection.
+ // We are not allowed to retrieve the prior projection, since we are not waiting for the
+ // user's consent.
+ assertThat(startReusedProjectionPreconditions()).isNull();
+ }
+
+ @Test
+ public void testCreateProjection_attemptReuse_priorProjectionGrant_waiting()
+ throws NameNotFoundException {
+ // Create a first projection.
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+
+ // Mark this projection as waiting for the user to review consent.
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+
+ // We are allowed to create another projection, reusing a prior grant if necessary.
MediaProjectionManagerService.MediaProjection secondProjection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ true);
+ startReusedProjectionPreconditions();
+ // This is a new projection, since we are waiting for the user's consent; simply provide
+ // the projection grant from before.
assertThat(secondProjection).isNotNull();
-
- // But this is a new projection.
- assertThat(secondProjection).isNotEqualTo(projection);
+ assertThat(secondProjection).isEqualTo(projection);
}
@Test
public void testCreateProjection_attemptReuse_priorProjectionGrant_waiting_differentPackage()
throws NameNotFoundException {
- MediaProjectionManagerService.MediaProjection projection =
- startProjectionPreconditions(/* packageAttemptedReusingGrantedConsent= */ false);
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
projection.start(mIMediaProjectionCallback);
// Mark this projection as not waiting.
@@ -213,8 +246,7 @@
// We are allowed to create another projection.
MediaProjectionManagerService.MediaProjection secondProjection =
mService.createProjectionInternal(UID + 10, PACKAGE_NAME + "foo",
- TYPE_MIRRORING, /* isPermanentGrant= */ true,
- UserHandle.CURRENT, /* packageAttemptedReusingGrantedConsent= */ true);
+ TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
assertThat(secondProjection).isNotNull();
@@ -366,6 +398,267 @@
assertThat(mService.isCurrentProjection(projection.asBinder())).isTrue();
}
+ @Test
+ public void testSetUserReviewGrantedConsentResult_noCurrentProjection() {
+ // Gracefully handle invocation without a current projection.
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY,
+ mock(IMediaProjection.class));
+ assertThat(mService.getActiveProjectionInfo()).isNull();
+ verify(mWindowManagerInternal, never()).setContentRecordingSession(any(
+ ContentRecordingSession.class));
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_projectionNotCurrent() throws Exception {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ // Some other token.
+ final IMediaProjection otherProjection = mock(IMediaProjection.class);
+ doReturn(mock(IBinder.class)).when(otherProjection).asBinder();
+ // Waiting for user to review consent.
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, otherProjection);
+
+ // Display result is ignored; only the first session is set.
+ verify(mWindowManagerInternal, times(1)).setContentRecordingSession(
+ eq(mWaitingDisplaySession));
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_projectionNull() throws Exception {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ // Some other token.
+ final IMediaProjection otherProjection = null;
+ // Waiting for user to review consent.
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, otherProjection);
+
+ // Display result is ignored; only the first session is set.
+ verify(mWindowManagerInternal, times(1)).setContentRecordingSession(
+ eq(mWaitingDisplaySession));
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_noVirtualDisplay() throws Exception {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ // Do not indicate that the virtual display was created.
+ ContentRecordingSession session = mWaitingDisplaySession;
+ session.setVirtualDisplayId(INVALID_DISPLAY);
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ // Waiting for user to review consent.
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ mService.setContentRecordingSession(session);
+
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
+ // A session is sent, indicating consent is granted to record but the virtual display isn't
+ // ready yet.
+ verify(mWindowManagerInternal, times(2)).setContentRecordingSession(
+ mSessionCaptor.capture());
+ // Examine latest value.
+ final ContentRecordingSession capturedSession = mSessionCaptor.getValue();
+ assertThat(capturedSession.isWaitingToRecord()).isFalse();
+ assertThat(capturedSession.getVirtualDisplayId()).isEqualTo(INVALID_DISPLAY);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_thenVirtualDisplayCreated() throws Exception {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ // Waiting for user to review consent.
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
+
+ // Virtual Display is finally created.
+ projection.notifyVirtualDisplayCreated(10);
+ verifySetSessionWithContent(ContentRecordingSession.RECORD_CONTENT_DISPLAY);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_unknown_updatedSession() throws Exception {
+ testSetUserReviewGrantedConsentResult_userCancelsSession(
+ /* isSetSessionSuccessful= */ true, UNKNOWN);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_unknown_failedToUpdateSession()
+ throws Exception {
+ testSetUserReviewGrantedConsentResult_userCancelsSession(
+ /* isSetSessionSuccessful= */ false, UNKNOWN);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_cancel_updatedSession() throws Exception {
+ testSetUserReviewGrantedConsentResult_userCancelsSession(
+ /* isSetSessionSuccessful= */ true, RECORD_CANCEL);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_cancel_failedToUpdateSession()
+ throws Exception {
+ testSetUserReviewGrantedConsentResult_userCancelsSession(
+ /* isSetSessionSuccessful= */ false, RECORD_CANCEL);
+ }
+
+ /**
+ * Executes and validates scenario where the consent result indicates the projection ends.
+ */
+ private void testSetUserReviewGrantedConsentResult_userCancelsSession(
+ boolean isSetSessionSuccessful, @ReviewGrantedConsentResult int consentResult)
+ throws Exception {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+ // Waiting for user to review consent.
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+
+ doReturn(isSetSessionSuccessful).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+
+ mService.setUserReviewGrantedConsentResult(consentResult, projection);
+ verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
+ mSessionCaptor.capture());
+ // Null value to stop session.
+ assertThat(mSessionCaptor.getValue()).isNull();
+ assertThat(mService.isCurrentProjection(projection.asBinder())).isFalse();
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_displayMirroring_startedSession()
+ throws NameNotFoundException {
+ testSetUserReviewGrantedConsentResult_startedSession(RECORD_CONTENT_DISPLAY,
+ ContentRecordingSession.RECORD_CONTENT_DISPLAY);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_displayMirroring_failedToStartSession()
+ throws NameNotFoundException {
+ testSetUserReviewGrantedConsentResult_failedToStartSession(RECORD_CONTENT_DISPLAY,
+ ContentRecordingSession.RECORD_CONTENT_DISPLAY);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_taskMirroring_startedSession()
+ throws NameNotFoundException {
+ testSetUserReviewGrantedConsentResult_startedSession(RECORD_CONTENT_TASK,
+ ContentRecordingSession.RECORD_CONTENT_TASK);
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_taskMirroring_failedToStartSession()
+ throws NameNotFoundException {
+ testSetUserReviewGrantedConsentResult_failedToStartSession(RECORD_CONTENT_TASK,
+ ContentRecordingSession.RECORD_CONTENT_TASK);
+ }
+
+ /**
+ * Executes and validates scenario where the consent result indicates the projection continues,
+ * and successfully started projection.
+ */
+ private void testSetUserReviewGrantedConsentResult_startedSession(
+ @ReviewGrantedConsentResult int consentResult,
+ @ContentRecordingSession.RecordContent int recordedContent)
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.setLaunchCookie(mock(IBinder.class));
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+ // Waiting for user to review consent.
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+
+ mService.setUserReviewGrantedConsentResult(consentResult, projection);
+ verifySetSessionWithContent(recordedContent);
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ }
+
+ /**
+ * Executes and validates scenario where the consent result indicates the projection continues,
+ * but unable to continue projection.
+ */
+ private void testSetUserReviewGrantedConsentResult_failedToStartSession(
+ @ReviewGrantedConsentResult int consentResult,
+ @ContentRecordingSession.RecordContent int recordedContent)
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+ // Waiting for user to review consent.
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ eq(mWaitingDisplaySession));
+ mService.setContentRecordingSession(mWaitingDisplaySession);
+
+ doReturn(false).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+
+ mService.setUserReviewGrantedConsentResult(consentResult, projection);
+ verifySetSessionWithContent(recordedContent);
+ assertThat(mService.isCurrentProjection(projection.asBinder())).isFalse();
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_displayMirroring_noPriorSession()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.setLaunchCookie(mock(IBinder.class));
+ projection.start(mIMediaProjectionCallback);
+ // Skip setting the prior session details.
+
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
+ // Result is ignored & session not updated.
+ verify(mWindowManagerInternal, never()).setContentRecordingSession(any(
+ ContentRecordingSession.class));
+ // Current session continues.
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ }
+
+ @Test
+ public void testSetUserReviewGrantedConsentResult_displayMirroring_sessionNotWaiting()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ projection.setLaunchCookie(mock(IBinder.class));
+ projection.start(mIMediaProjectionCallback);
+ // Session is not waiting for user's consent.
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+ mService.setContentRecordingSession(DISPLAY_SESSION);
+
+ doReturn(true).when(mWindowManagerInternal).setContentRecordingSession(
+ any(ContentRecordingSession.class));
+
+ mService.setUserReviewGrantedConsentResult(RECORD_CONTENT_DISPLAY, projection);
+ // Result is ignored; only the original session was ever sent.
+ verify(mWindowManagerInternal).setContentRecordingSession(eq(
+ DISPLAY_SESSION));
+ // Current session continues.
+ assertThat(mService.isCurrentProjection(projection)).isTrue();
+ }
+
+ private void verifySetSessionWithContent(@ContentRecordingSession.RecordContent int content) {
+ verify(mWindowManagerInternal, atLeastOnce()).setContentRecordingSession(
+ mSessionCaptor.capture());
+ assertThat(mSessionCaptor.getValue()).isNotNull();
+ assertThat(mSessionCaptor.getValue().getContentToRecord()).isEqualTo(content);
+ }
+
// Set up preconditions for creating a projection.
private MediaProjectionManagerService.MediaProjection createProjectionPreconditions(
MediaProjectionManagerService service)
@@ -373,14 +666,7 @@
doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
any(ApplicationInfoFlags.class), any(UserHandle.class));
return service.createProjectionInternal(UID, PACKAGE_NAME,
- TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT,
- /* packageAttemptedReusingGrantedConsent= */ false);
- }
-
- // Set up preconditions for creating a projection.
- private MediaProjectionManagerService.MediaProjection createProjectionPreconditions()
- throws NameNotFoundException {
- return createProjectionPreconditions(mService);
+ TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT);
}
// Set up preconditions for starting a projection, with no foreground service requirements.
@@ -391,19 +677,6 @@
return createProjectionPreconditions(service);
}
- // Set up preconditions for starting a projection, specifying if it is possible to reuse the
- // the current projection.
- private MediaProjectionManagerService.MediaProjection startProjectionPreconditions(
- boolean packageAttemptedReusingGrantedConsent)
- throws NameNotFoundException {
- mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
- doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
- any(ApplicationInfoFlags.class), any(UserHandle.class));
- return mService.createProjectionInternal(UID, PACKAGE_NAME,
- TYPE_MIRRORING, /* isPermanentGrant= */ true, UserHandle.CURRENT,
- packageAttemptedReusingGrantedConsent);
- }
-
// Set up preconditions for starting a projection, with no foreground service requirements.
private MediaProjectionManagerService.MediaProjection startProjectionPreconditions()
throws NameNotFoundException {
@@ -411,6 +684,15 @@
return createProjectionPreconditions(mService);
}
+ // Set up preconditions for starting a projection, retrieving a pre-existing projection.
+ private MediaProjectionManagerService.MediaProjection startReusedProjectionPreconditions()
+ throws NameNotFoundException {
+ mAppInfo.privateFlags |= PRIVATE_FLAG_PRIVILEGED;
+ doReturn(mAppInfo).when(mPackageManager).getApplicationInfoAsUser(anyString(),
+ any(ApplicationInfoFlags.class), any(UserHandle.class));
+ return mService.getProjectionInternal(UID, PACKAGE_NAME);
+ }
+
private static class FakeIMediaProjectionCallback extends IMediaProjectionCallback.Stub {
@Override
public void onStop() throws RemoteException {
diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
index 8f0a5e6..bf6901e 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java
@@ -21,6 +21,8 @@
import static android.view.KeyEvent.KEYCODE_C;
import static android.view.KeyEvent.KEYCODE_CTRL_LEFT;
import static android.view.KeyEvent.KEYCODE_E;
+import static android.view.KeyEvent.KEYCODE_ENTER;
+import static android.view.KeyEvent.KEYCODE_H;
import static android.view.KeyEvent.KEYCODE_K;
import static android.view.KeyEvent.KEYCODE_M;
import static android.view.KeyEvent.KEYCODE_META_LEFT;
@@ -164,4 +166,24 @@
sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ALT_LEFT}, 0);
mPhoneWindowManager.assertToggleCapsLock();
}
+
+ /**
+ * META + H to go to homescreen
+ */
+ @Test
+ public void testMetaH() {
+ mPhoneWindowManager.overrideLaunchHome();
+ sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_H}, 0);
+ mPhoneWindowManager.assertGoToHomescreen();
+ }
+
+ /**
+ * META + ENTER to go to homescreen
+ */
+ @Test
+ public void testMetaEnter() {
+ mPhoneWindowManager.overrideLaunchHome();
+ sendKeyCombination(new int[]{KEYCODE_META_LEFT, KEYCODE_ENTER}, 0);
+ mPhoneWindowManager.assertGoToHomescreen();
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
index 6368f47..676bfb0 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java
@@ -117,9 +117,9 @@
throw new RuntimeException(e);
}
- for (KeyEvent event: events) {
+ for (int i = count - 1; i >= 0; i--) {
final long eventTime = SystemClock.uptimeMillis();
- final int keyCode = event.getKeyCode();
+ final int keyCode = keyCodes[i];
final KeyEvent upEvent = new KeyEvent(downTime, eventTime, KeyEvent.ACTION_UP, keyCode,
0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /*scancode*/, 0 /*flags*/,
InputDevice.SOURCE_KEYBOARD);
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index a2ee8a4..2665e19 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -353,6 +353,10 @@
() -> LocalServices.getService(eq(StatusBarManagerInternal.class)));
}
+ void overrideLaunchHome() {
+ doNothing().when(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
+ }
+
/**
* Below functions will check the policy behavior could be invoked.
*/
@@ -480,4 +484,9 @@
transitionCaptor.getValue().onAppTransitionFinishedLocked(any());
verify(mPhoneWindowManager).lockNow(null);
}
+
+ void assertGoToHomescreen() {
+ waitForIdle();
+ verify(mPhoneWindowManager).launchHomeFromHotKey(anyInt());
+ }
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index fb4f2ee..1cec0ef 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -41,8 +41,9 @@
import android.annotation.NonNull;
import android.app.WindowConfiguration;
-import android.content.res.Configuration;
+import android.content.ContentResolver;
import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
import android.view.Display;
import android.view.DisplayInfo;
import android.view.Surface;
@@ -439,6 +440,7 @@
public void testDisplayWindowSettingsAppliedOnDisplayReady() {
// Set forced densities for two displays in DisplayWindowSettings
final DisplayContent dc = createMockSimulatedDisplay();
+ final ContentResolver contentResolver = useFakeSettingsProvider();
mDisplayWindowSettings.setForcedDensity(mPrimaryDisplay.getDisplayInfo(), 123,
0 /* userId */);
mDisplayWindowSettings.setForcedDensity(dc.getDisplayInfo(), 456, 0 /* userId */);
@@ -450,15 +452,21 @@
assertFalse(mPrimaryDisplay.mWaitingForConfig);
assertFalse(dc.mWaitingForConfig);
+ final int invalidW = Integer.MAX_VALUE;
+ final int invalidH = Integer.MAX_VALUE;
+ // Verify that applyForcedPropertiesForDefaultDisplay() handles invalid size request.
+ Settings.Global.putString(contentResolver, Settings.Global.DISPLAY_SIZE_FORCED,
+ invalidW + "," + invalidH);
// Notify WM that the displays are ready and check that they are reconfigured.
mWm.displayReady();
waitUntilHandlersIdle();
- final Configuration config = new Configuration();
- mPrimaryDisplay.computeScreenConfiguration(config);
- assertEquals(123, config.densityDpi);
- dc.computeScreenConfiguration(config);
- assertEquals(456, config.densityDpi);
+ // Density is set successfully.
+ assertEquals(123, mPrimaryDisplay.getConfiguration().densityDpi);
+ assertEquals(456, dc.getConfiguration().densityDpi);
+ // Invalid size won't be applied.
+ assertNotEquals(invalidW, mPrimaryDisplay.mBaseDisplayWidth);
+ assertNotEquals(invalidH, mPrimaryDisplay.mBaseDisplayHeight);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index f85cdf0..07244a4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -66,6 +66,7 @@
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.content.ComponentName;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -81,6 +82,7 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.provider.Settings;
import android.service.voice.IVoiceInteractionSession;
import android.util.SparseArray;
import android.view.Display;
@@ -109,6 +111,7 @@
import com.android.internal.policy.AttributeCache;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.test.FakeSettingsProvider;
import com.android.server.wm.DisplayWindowSettings.SettingsProvider.SettingsEntry;
import org.junit.After;
@@ -146,6 +149,7 @@
WindowManagerService mWm;
private final IWindow mIWindow = new TestIWindow();
private Session mMockSession;
+ private boolean mUseFakeSettingsProvider;
DisplayInfo mDisplayInfo = new DisplayInfo();
DisplayContent mDefaultDisplay;
@@ -272,16 +276,9 @@
@After
public void tearDown() throws Exception {
- // Revert back to device overrides.
- mAtm.mWindowManager.mLetterboxConfiguration.resetFixedOrientationLetterboxAspectRatio();
- mAtm.mWindowManager.mLetterboxConfiguration.resetLetterboxHorizontalPositionMultiplier();
- mAtm.mWindowManager.mLetterboxConfiguration.resetLetterboxVerticalPositionMultiplier();
- mAtm.mWindowManager.mLetterboxConfiguration.resetIsHorizontalReachabilityEnabled();
- mAtm.mWindowManager.mLetterboxConfiguration.resetIsVerticalReachabilityEnabled();
- mAtm.mWindowManager.mLetterboxConfiguration
- .resetIsSplitScreenAspectRatioForUnresizableAppsEnabled();
- mAtm.mWindowManager.mLetterboxConfiguration
- .resetIsDisplayAspectRatioEnabledForFixedOrientationLetterbox();
+ if (mUseFakeSettingsProvider) {
+ FakeSettingsProvider.clearSettingsProvider();
+ }
}
/**
@@ -428,6 +425,17 @@
// Called before display is created.
}
+ /** Avoid writing values to real Settings. */
+ ContentResolver useFakeSettingsProvider() {
+ mUseFakeSettingsProvider = true;
+ FakeSettingsProvider.clearSettingsProvider();
+ final FakeSettingsProvider provider = new FakeSettingsProvider();
+ // SystemServicesTestRule#setUpSystemCore has called spyOn for the ContentResolver.
+ final ContentResolver resolver = mContext.getContentResolver();
+ doReturn(provider.getIContentProvider()).when(resolver).acquireProvider(Settings.AUTHORITY);
+ return resolver;
+ }
+
private WindowState createCommonWindow(WindowState parent, int type, String name) {
final WindowState win = createWindow(parent, type, name);
// Prevent common windows from been IME targets.
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
index 07dc1c6..18d0c5a 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerHelper.java
@@ -49,6 +49,7 @@
import android.telephony.TelephonyManager;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import java.io.FileDescriptor;
@@ -129,6 +130,9 @@
private final Function<SoundTrigger.StatusListener, SoundTriggerModule> mModuleProvider;
private final Supplier<List<ModuleProperties>> mModulePropertiesProvider;
+ @GuardedBy("mLock")
+ private boolean mIsDetached = false;
+
SoundTriggerHelper(Context context,
@NonNull Function<SoundTrigger.StatusListener, SoundTriggerModule> moduleProvider,
int moduleId,
@@ -184,7 +188,7 @@
* recognition.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
+ public int startGenericRecognition(UUID modelId, GenericSoundModel soundModel,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
boolean runInBatterySaverMode) {
MetricsLogger.count(mContext, "sth_start_recognition", 1);
@@ -195,6 +199,9 @@
}
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = getOrCreateGenericModelDataLocked(modelId);
if (modelData == null) {
Slog.w(TAG, "Irrecoverable error occurred, check UUID / sound model data.");
@@ -214,7 +221,7 @@
* @param callback The callback for the recognition events related to the given keyphrase.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int startKeyphraseRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
+ public int startKeyphraseRecognition(int keyphraseId, KeyphraseSoundModel soundModel,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
boolean runInBatterySaverMode) {
synchronized (mLock) {
@@ -223,6 +230,10 @@
return STATUS_ERROR;
}
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
+
if (DBG) {
Slog.d(TAG, "startKeyphraseRecognition for keyphraseId=" + keyphraseId
+ " soundModel=" + soundModel + ", callback=" + callback.asBinder()
@@ -311,7 +322,7 @@
* for the recognition.
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int startRecognition(SoundModel soundModel, ModelData modelData,
+ private int startRecognition(SoundModel soundModel, ModelData modelData,
IRecognitionStatusCallback callback, RecognitionConfig recognitionConfig,
int keyphraseId, boolean runInBatterySaverMode) {
synchronized (mLock) {
@@ -385,7 +396,7 @@
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int stopGenericRecognition(UUID modelId, IRecognitionStatusCallback callback) {
+ public int stopGenericRecognition(UUID modelId, IRecognitionStatusCallback callback) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_stop_recognition", 1);
if (callback == null || modelId == null) {
@@ -393,7 +404,9 @@
modelId);
return STATUS_ERROR;
}
-
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null || !modelData.isGenericModel()) {
Slog.w(TAG, "Attempting stopRecognition on invalid model with id:" + modelId);
@@ -418,7 +431,7 @@
*
* @return One of {@link #STATUS_ERROR} or {@link #STATUS_OK}.
*/
- int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
+ public int stopKeyphraseRecognition(int keyphraseId, IRecognitionStatusCallback callback) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_stop_recognition", 1);
if (callback == null) {
@@ -426,7 +439,9 @@
keyphraseId);
return STATUS_ERROR;
}
-
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
if (modelData == null || !modelData.isKeyphraseModel()) {
Slog.w(TAG, "No model exists for given keyphrase Id " + keyphraseId);
@@ -538,6 +553,11 @@
}
public ModuleProperties getModuleProperties() {
+ synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
+ }
for (ModuleProperties moduleProperties : mModulePropertiesProvider.get()) {
if (moduleProperties.getId() == mModuleId) {
return moduleProperties;
@@ -547,7 +567,7 @@
return null;
}
- int unloadKeyphraseSoundModel(int keyphraseId) {
+ public int unloadKeyphraseSoundModel(int keyphraseId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_unload_keyphrase_sound_model", 1);
ModelData modelData = getKeyphraseModelDataLocked(keyphraseId);
@@ -555,7 +575,9 @@
|| !modelData.isKeyphraseModel()) {
return STATUS_ERROR;
}
-
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
// Stop recognition if it's the current one.
modelData.setRequested(false);
int status = updateRecognitionLocked(modelData, false);
@@ -574,12 +596,15 @@
}
}
- int unloadGenericSoundModel(UUID modelId) {
+ public int unloadGenericSoundModel(UUID modelId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_unload_generic_sound_model", 1);
if (modelId == null || mModule == null) {
return STATUS_ERROR;
}
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null || !modelData.isGenericModel()) {
Slog.w(TAG, "Unload error: Attempting unload invalid generic model with id:" +
@@ -615,19 +640,25 @@
}
}
- boolean isRecognitionRequested(UUID modelId) {
+ public boolean isRecognitionRequested(UUID modelId) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = mModelDataMap.get(modelId);
return modelData != null && modelData.isRequested();
}
}
- int getGenericModelState(UUID modelId) {
+ public int getGenericModelState(UUID modelId) {
synchronized (mLock) {
MetricsLogger.count(mContext, "sth_get_generic_model_state", 1);
if (modelId == null || mModule == null) {
return STATUS_ERROR;
}
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
ModelData modelData = mModelDataMap.get(modelId);
if (modelData == null || !modelData.isGenericModel()) {
Slog.w(TAG, "GetGenericModelState error: Invalid generic model id:" +
@@ -647,19 +678,20 @@
}
}
- int getKeyphraseModelState(UUID modelId) {
- Slog.w(TAG, "GetKeyphraseModelState error: Not implemented");
- return STATUS_ERROR;
- }
-
- int setParameter(UUID modelId, @ModelParams int modelParam, int value) {
+ public int setParameter(UUID modelId, @ModelParams int modelParam, int value) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return setParameterLocked(mModelDataMap.get(modelId), modelParam, value);
}
}
- int setKeyphraseParameter(int keyphraseId, @ModelParams int modelParam, int value) {
+ public int setKeyphraseParameter(int keyphraseId, @ModelParams int modelParam, int value) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return setParameterLocked(getKeyphraseModelDataLocked(keyphraseId), modelParam, value);
}
}
@@ -678,14 +710,20 @@
return mModule.setParameter(modelData.getHandle(), modelParam, value);
}
- int getParameter(@NonNull UUID modelId, @ModelParams int modelParam) {
+ public int getParameter(@NonNull UUID modelId, @ModelParams int modelParam) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return getParameterLocked(mModelDataMap.get(modelId), modelParam);
}
}
- int getKeyphraseParameter(int keyphraseId, @ModelParams int modelParam) {
+ public int getKeyphraseParameter(int keyphraseId, @ModelParams int modelParam) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return getParameterLocked(getKeyphraseModelDataLocked(keyphraseId), modelParam);
}
}
@@ -707,15 +745,21 @@
}
@Nullable
- ModelParamRange queryParameter(@NonNull UUID modelId, @ModelParams int modelParam) {
+ public ModelParamRange queryParameter(@NonNull UUID modelId, @ModelParams int modelParam) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return queryParameterLocked(mModelDataMap.get(modelId), modelParam);
}
}
@Nullable
- ModelParamRange queryKeyphraseParameter(int keyphraseId, @ModelParams int modelParam) {
+ public ModelParamRange queryKeyphraseParameter(int keyphraseId, @ModelParams int modelParam) {
synchronized (mLock) {
+ if (mIsDetached) {
+ throw new IllegalStateException("SoundTriggerHelper has been detached");
+ }
return queryParameterLocked(getKeyphraseModelDataLocked(keyphraseId), modelParam);
}
}
@@ -1115,12 +1159,14 @@
*/
public void detach() {
synchronized (mLock) {
+ if (mIsDetached) return;
for (ModelData model : mModelDataMap.values()) {
forceStopAndUnloadModelLocked(model, null);
}
mModelDataMap.clear();
internalClearGlobalStateLocked();
if (mModule != null) {
+ mIsDetached = true;
mModule.detach();
mModule = null;
}
@@ -1289,7 +1335,7 @@
* @param modelData Model data to be used for recognition
* @return True if device state allows recognition to run, false if not.
*/
- boolean isRecognitionAllowedByPowerState(ModelData modelData) {
+ private boolean isRecognitionAllowedByPowerState(ModelData modelData) {
return mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_ALL_ENABLED
|| (mSoundTriggerPowerSaveMode == PowerManager.SOUND_TRIGGER_MODE_CRITICAL_ONLY
&& modelData.shouldRunInBatterySaverMode());
diff --git a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
index 790be8d..46e634f 100644
--- a/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
+++ b/services/voiceinteraction/java/com/android/server/soundtrigger/SoundTriggerService.java
@@ -1663,6 +1663,11 @@
}
@Override
+ public void detach() {
+ mSoundTriggerHelper.detach();
+ }
+
+ @Override
public int unloadKeyphraseModel(int keyphraseId) {
return mSoundTriggerHelper.unloadKeyphraseSoundModel(keyphraseId);
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java
index dd9fee3..0ef2f06 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionBinderProxy.java
@@ -69,4 +69,9 @@
public SoundTrigger.ModelParamRange queryParameter(int i, int i1) throws RemoteException {
return mDelegate.queryParameter(i, i1);
}
+
+ @Override
+ public void detach() throws RemoteException {
+ mDelegate.detach();
+ }
}
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java
index c0c3e6f..0f8a945 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/SoundTriggerSessionPermissionsDecorator.java
@@ -113,6 +113,15 @@
"This object isn't intended to be used as a Binder.");
}
+ @Override
+ public void detach() {
+ try {
+ mDelegate.detach();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+
// TODO: Share this code with SoundTriggerMiddlewarePermission.
private boolean isHoldingPermissions() {
try {
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 1d7b966..bb50c79 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -1856,6 +1856,11 @@
"This object isn't intended to be used as a Binder.");
}
+ @Override
+ public void detach() {
+ mSession.detach();
+ }
+
private int unloadKeyphraseModel(int keyphraseId) {
final long caller = Binder.clearCallingIdentity();
try {
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 282b64d..18e4c37 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -3008,4 +3008,14 @@
* @return {@code true} if the timeout duration is set successfully, {@code false} otherwise.
*/
boolean setSatelliteListeningTimeoutDuration(in long timeoutMillis);
+
+ /**
+ * This API can be used by only CTS to update satellite pointing UI app package and class names.
+ *
+ * @param packageName The package name of the satellite pointing UI app.
+ * @param className The class name of the satellite pointing UI app.
+ * @return {@code true} if the satellite pointing UI app package and class is set successfully,
+ * {@code false} otherwise.
+ */
+ boolean setSatellitePointingUiClassName(in String packageName, in String className);
}