Merge cherrypicks of ['googleplex-android-review.googlesource.com/29878325', 'googleplex-android-review.googlesource.com/29878326', 'googleplex-android-review.googlesource.com/30153909', 'googleplex-android-review.googlesource.com/30519371'] into 24Q4-release.
Change-Id: I1325874594ea73e1837f19e6797d1753b46898a3
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
index 77337d3..a981e20 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt
@@ -18,6 +18,7 @@
import android.content.Intent
import android.content.mockedContext
+import android.content.res.Resources
import android.hardware.fingerprint.FingerprintManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -41,13 +42,16 @@
import com.android.systemui.plugins.ActivityStarter.OnDismissAction
import com.android.systemui.plugins.activityStarter
import com.android.systemui.power.data.repository.fakePowerRepository
+import com.android.systemui.res.R
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
@@ -55,6 +59,7 @@
import org.mockito.ArgumentMatchers.isNull
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -63,8 +68,8 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val underTest = kosmos.occludingAppDeviceEntryInteractor
-
+ private lateinit var underTest: OccludingAppDeviceEntryInteractor
+ private lateinit var mockedResources: Resources
private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val bouncerRepository = kosmos.keyguardBouncerRepository
@@ -74,9 +79,18 @@
private val mockedContext = kosmos.mockedContext
private val mockedActivityStarter = kosmos.activityStarter
+ @Before
+ fun setup() {
+ mockedResources = mock<Resources>()
+ whenever(mockedContext.resources).thenReturn(mockedResources)
+ whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps))
+ .thenReturn(true)
+ }
+
@Test
fun fingerprintSuccess_goToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(true)
fingerprintAuthRepository.setAuthenticationStatus(
SuccessFingerprintAuthenticationStatus(0, true)
@@ -86,8 +100,23 @@
}
@Test
+ fun fingerprintSuccess_configOff_doesNotGoToHomeScreen() =
+ testScope.runTest {
+ whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps))
+ .thenReturn(false)
+ underTest = kosmos.occludingAppDeviceEntryInteractor
+ givenOnOccludingApp(true)
+ fingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ verifyNeverGoToHomeScreen()
+ }
+
+ @Test
fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(true)
powerRepository.setInteractive(false)
fingerprintAuthRepository.setAuthenticationStatus(
@@ -100,6 +129,7 @@
@Test
fun fingerprintSuccess_dreaming_doesNotGoToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(true)
keyguardRepository.setDreaming(true)
fingerprintAuthRepository.setAuthenticationStatus(
@@ -112,6 +142,7 @@
@Test
fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(false)
fingerprintAuthRepository.setAuthenticationStatus(
SuccessFingerprintAuthenticationStatus(0, true)
@@ -123,11 +154,12 @@
@Test
fun lockout_goToHomeScreenOnDismissAction() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(true)
fingerprintAuthRepository.setAuthenticationStatus(
ErrorFingerprintAuthenticationStatus(
FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
- "lockoutTest"
+ "lockoutTest",
)
)
runCurrent()
@@ -137,11 +169,12 @@
@Test
fun lockout_notOnOccludingApp_neverGoToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(false)
fingerprintAuthRepository.setAuthenticationStatus(
ErrorFingerprintAuthenticationStatus(
FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
- "lockoutTest"
+ "lockoutTest",
)
)
runCurrent()
@@ -151,11 +184,12 @@
@Test
fun lockout_onOccludingApp_onCommunal_neverGoToHomeScreen() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
givenOnOccludingApp(isOnOccludingApp = true, isOnCommunal = true)
fingerprintAuthRepository.setAuthenticationStatus(
ErrorFingerprintAuthenticationStatus(
FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
- "lockoutTest"
+ "lockoutTest",
)
)
runCurrent()
@@ -165,6 +199,7 @@
@Test
fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
val message by collectLastValue(underTest.message)
givenOnOccludingApp(true)
@@ -186,6 +221,7 @@
@Test
fun message_fpErrorHelpFailOnOccludingApp() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
val message by collectLastValue(underTest.message)
givenOnOccludingApp(true)
@@ -218,6 +254,7 @@
@Test
fun message_fpError_lockoutFilteredOut() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
val message by collectLastValue(underTest.message)
givenOnOccludingApp(true)
@@ -246,6 +283,7 @@
@Test
fun noMessage_fpErrorsWhileDozing() =
testScope.runTest {
+ underTest = kosmos.occludingAppDeviceEntryInteractor
val message by collectLastValue(underTest.message)
givenOnOccludingApp(true)
@@ -254,7 +292,7 @@
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.OCCLUDED,
to = KeyguardState.DOZING,
- testScope
+ testScope,
)
runCurrent()
@@ -283,7 +321,7 @@
private suspend fun givenOnOccludingApp(
isOnOccludingApp: Boolean,
- isOnCommunal: Boolean = false
+ isOnCommunal: Boolean = false,
) {
powerRepository.setInteractive(true)
keyguardRepository.setIsDozing(false)
@@ -305,13 +343,13 @@
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.OCCLUDED,
- testScope
+ testScope,
)
} else {
kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps(
from = KeyguardState.OCCLUDED,
to = KeyguardState.LOCKSCREEN,
- testScope
+ testScope,
)
}
}
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 38ef0e9..78b2aef 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -324,6 +324,9 @@
<!-- Whether to show the full screen user switcher. -->
<bool name="config_enableFullscreenUserSwitcher">false</bool>
+ <!-- Whether to go to the launcher when unlocking via an occluding app -->
+ <bool name="config_goToHomeFromOccludedApps">false</bool>
+
<!-- Determines whether the shell features all run on another thread. -->
<bool name="config_enableShellMainThread">true</bool>
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
index f90f02a..9f4b1cc 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt
@@ -34,6 +34,7 @@
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.power.domain.interactor.PowerInteractor
+import com.android.systemui.res.R
import com.android.systemui.util.kotlin.combine
import com.android.systemui.util.kotlin.sample
import javax.inject.Inject
@@ -123,19 +124,28 @@
.ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null))
init {
- scope.launch {
- // On fingerprint success when the screen is on and not dreaming, go to the home screen
- fingerprintUnlockSuccessEvents
- .sample(
- combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair)
- )
- .collect { (interactive, dreaming) ->
- if (interactive && !dreaming) {
- goToHomeScreen()
+ // This seems undesirable in most cases, except when a video is playing and can PiP when
+ // unlocked. It was originally added for tablets, so allow it there
+ if (context.resources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) {
+ scope.launch {
+ // On fingerprint success when the screen is on and not dreaming, go to the home
+ // screen
+ fingerprintUnlockSuccessEvents
+ .sample(
+ combine(
+ powerInteractor.isInteractive,
+ keyguardInteractor.isDreaming,
+ ::Pair,
+ )
+ )
+ .collect { (interactive, dreaming) ->
+ if (interactive && !dreaming) {
+ goToHomeScreen()
+ }
+ // don't go to the home screen if the authentication is from
+ // AOD/dozing/off/dreaming
}
- // don't go to the home screen if the authentication is from
- // AOD/dozing/off/dreaming
- }
+ }
}
scope.launch {
diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
index 0fd22c5..cdfb740 100644
--- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java
+++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java
@@ -679,6 +679,8 @@
elapsed = System.currentTimeMillis() - start;
if (elapsed >= SET_COMMUNICATION_DEVICE_TIMEOUT_MS) {
Log.e(TAG, "Timeout waiting for communication device update.");
+ // reset counter to avoid sticky out of sync condition
+ mCommunicationDeviceUpdateCount = 0;
break;
}
}
@@ -1321,9 +1323,9 @@
sendLMsgNoDelay(MSG_II_SET_LE_AUDIO_OUT_VOLUME, SENDMSG_REPLACE, info);
}
- /*package*/ void postSetModeOwner(int mode, int pid, int uid) {
- sendLMsgNoDelay(MSG_I_SET_MODE_OWNER, SENDMSG_REPLACE,
- new AudioModeInfo(mode, pid, uid));
+ /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) {
+ sendLMsgNoDelay(signal ? MSG_L_SET_MODE_OWNER_SIGNAL : MSG_L_SET_MODE_OWNER,
+ SENDMSG_REPLACE, new AudioModeInfo(mode, pid, uid));
}
/*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) {
@@ -2025,7 +2027,8 @@
mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1);
}
break;
- case MSG_I_SET_MODE_OWNER:
+ case MSG_L_SET_MODE_OWNER:
+ case MSG_L_SET_MODE_OWNER_SIGNAL:
synchronized (mSetModeLock) {
synchronized (mDeviceStateLock) {
int btScoRequesterUid = bluetoothScoRequestOwnerUid();
@@ -2036,6 +2039,9 @@
}
}
}
+ if (msg.what == MSG_L_SET_MODE_OWNER_SIGNAL) {
+ mAudioService.decrementAudioModeResetCount();
+ }
break;
case MSG_L_SET_COMMUNICATION_DEVICE_FOR_CLIENT:
@@ -2224,7 +2230,8 @@
private static final int MSG_REPORT_NEW_ROUTES = 13;
private static final int MSG_II_SET_HEARING_AID_VOLUME = 14;
private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15;
- private static final int MSG_I_SET_MODE_OWNER = 16;
+ private static final int MSG_L_SET_MODE_OWNER = 16;
+ private static final int MSG_L_SET_MODE_OWNER_SIGNAL = 17;
private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22;
private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index e1909d9..8abe121 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -455,7 +455,7 @@
private static final int MSG_UPDATE_AUDIO_MODE = 36;
private static final int MSG_RECORDING_CONFIG_CHANGE = 37;
private static final int MSG_BT_DEV_CHANGED = 38;
-
+ private static final int MSG_UPDATE_AUDIO_MODE_SIGNAL = 39;
private static final int MSG_DISPATCH_AUDIO_MODE = 40;
private static final int MSG_ROUTING_UPDATED = 41;
private static final int MSG_INIT_HEADTRACKING_SENSORS = 42;
@@ -1918,7 +1918,7 @@
// Restore call state
synchronized (mDeviceBroker.mSetModeLock) {
onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
- mContext.getPackageName(), true /*force*/);
+ mContext.getPackageName(), true /*force*/, false /*signal*/);
}
final int forSys;
synchronized (mSettingsLock) {
@@ -4746,17 +4746,45 @@
}
}
if (updateAudioMode) {
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- existingMsgPolicy,
- AudioSystem.MODE_CURRENT,
- android.os.Process.myPid(),
- mContext.getPackageName(),
- delay);
+ postUpdateAudioMode(existingMsgPolicy, AudioSystem.MODE_CURRENT,
+ android.os.Process.myPid(), mContext.getPackageName(),
+ false /*signal*/, delay);
}
}
}
+ static class UpdateAudioModeInfo {
+ UpdateAudioModeInfo(int mode, int pid, String packageName) {
+ mMode = mode;
+ mPid = pid;
+ mPackageName = packageName;
+ }
+ private final int mMode;
+ private final int mPid;
+ private final String mPackageName;
+
+ int getMode() {
+ return mMode;
+ }
+ int getPid() {
+ return mPid;
+ }
+ String getPackageName() {
+ return mPackageName;
+ }
+ }
+
+ void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName,
+ boolean signal, int delay) {
+ synchronized (mAudioModeResetLock) {
+ if (signal) {
+ mAudioModeResetCount++;
+ }
+ sendMsg(mAudioHandler, signal ? MSG_UPDATE_AUDIO_MODE_SIGNAL : MSG_UPDATE_AUDIO_MODE,
+ msgPolicy, 0, 0, new UpdateAudioModeInfo(mode, pid, packageName), delay);
+ }
+ }
+
private final IRecordingConfigDispatcher mVoiceRecordingActivityMonitor =
new IRecordingConfigDispatcher.Stub() {
@Override
@@ -6155,13 +6183,9 @@
} else {
SetModeDeathHandler h = mSetModeDeathHandlers.get(index);
mSetModeDeathHandlers.remove(index);
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- SENDMSG_QUEUE,
- AudioSystem.MODE_CURRENT,
- android.os.Process.myPid(),
- mContext.getPackageName(),
- 0);
+ postUpdateAudioMode(SENDMSG_QUEUE, AudioSystem.MODE_CURRENT,
+ android.os.Process.myPid(), mContext.getPackageName(),
+ false /*signal*/, 0);
}
}
}
@@ -6407,19 +6431,14 @@
}
}
- sendMsg(mAudioHandler,
- MSG_UPDATE_AUDIO_MODE,
- SENDMSG_REPLACE,
- mode,
- pid,
- callingPackage,
- 0);
+ postUpdateAudioMode(SENDMSG_REPLACE, mode, pid, callingPackage,
+ hasModifyPhoneStatePermission && mode == AudioSystem.MODE_NORMAL, 0);
}
}
@GuardedBy("mDeviceBroker.mSetModeLock")
void onUpdateAudioMode(int requestedMode, int requesterPid, String requesterPackage,
- boolean force) {
+ boolean force, boolean signal) {
if (requestedMode == AudioSystem.MODE_CURRENT) {
requestedMode = getMode();
}
@@ -6434,7 +6453,7 @@
}
if (DEBUG_MODE) {
Log.v(TAG, "onUpdateAudioMode() new mode: " + mode + ", current mode: "
- + mMode.get() + " requested mode: " + requestedMode);
+ + mMode.get() + " requested mode: " + requestedMode + " signal: " + signal);
}
if (mode != mMode.get() || force) {
int status = AudioSystem.SUCCESS;
@@ -6480,8 +6499,11 @@
// when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
// connections not started by the application changing the mode when pid changes
- mDeviceBroker.postSetModeOwner(mode, pid, uid);
+ mDeviceBroker.postSetModeOwner(mode, pid, uid, signal);
} else {
+ // reset here to avoid sticky out of sync condition (would have been reset
+ // by AudioDeviceBroker processing MSG_L_SET_MODE_OWNER_SIGNAL message)
+ resetAudioModeResetCount();
Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode);
}
}
@@ -10162,7 +10184,7 @@
h.setRecordingActive(isRecordingActiveForUid(h.getUid()));
if (wasActive != h.isActive()) {
onUpdateAudioMode(AudioSystem.MODE_CURRENT, android.os.Process.myPid(),
- mContext.getPackageName(), false /*force*/);
+ mContext.getPackageName(), false /*force*/, false /*signal*/);
}
}
break;
@@ -10191,8 +10213,11 @@
break;
case MSG_UPDATE_AUDIO_MODE:
+ case MSG_UPDATE_AUDIO_MODE_SIGNAL:
synchronized (mDeviceBroker.mSetModeLock) {
- onUpdateAudioMode(msg.arg1, msg.arg2, (String) msg.obj, false /*force*/);
+ UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj;
+ onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(),
+ false /*force*/, msg.what == MSG_UPDATE_AUDIO_MODE_SIGNAL);
}
break;
@@ -10895,9 +10920,68 @@
return AudioManager.AUDIOFOCUS_REQUEST_FAILED;
}
mmi.record();
+ //delay abandon focus requests from Telecom if an audio mode reset from Telecom
+ // is still being processed
+ final boolean abandonFromTelecom = (mContext.checkCallingOrSelfPermission(
+ MODIFY_PHONE_STATE) == PackageManager.PERMISSION_GRANTED)
+ && ((aa != null && aa.getUsage() == AudioAttributes.USAGE_VOICE_COMMUNICATION)
+ || AudioSystem.IN_VOICE_COMM_FOCUS_ID.equals(clientId));
+ if (abandonFromTelecom) {
+ synchronized (mAudioModeResetLock) {
+ final long start = java.lang.System.currentTimeMillis();
+ long elapsed = 0;
+ while (mAudioModeResetCount > 0) {
+ if (DEBUG_MODE) {
+ Log.i(TAG, "Abandon focus from Telecom, waiting for mode change");
+ }
+ try {
+ mAudioModeResetLock.wait(
+ AUDIO_MODE_RESET_TIMEOUT_MS - elapsed);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Interrupted while waiting for audio mode reset");
+ }
+ elapsed = java.lang.System.currentTimeMillis() - start;
+ if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) {
+ Log.e(TAG, "Timeout waiting for audio mode reset");
+ // reset count to avoid sticky out of sync state.
+ resetAudioModeResetCount();
+ break;
+ }
+ }
+ if (DEBUG_MODE && elapsed != 0) {
+ Log.i(TAG, "Abandon focus from Telecom done waiting");
+ }
+ }
+ }
return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName);
}
+ /** synchronization between setMode(NORMAL) and abandonAudioFocus() from Telecom */
+ private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000;
+
+ private final Object mAudioModeResetLock = new Object();
+
+ @GuardedBy("mAudioModeResetLock")
+ private int mAudioModeResetCount = 0;
+
+ void decrementAudioModeResetCount() {
+ synchronized (mAudioModeResetLock) {
+ if (mAudioModeResetCount > 0) {
+ mAudioModeResetCount--;
+ } else {
+ Log.w(TAG, "mAudioModeResetCount already 0");
+ }
+ mAudioModeResetLock.notify();
+ }
+ }
+
+ private void resetAudioModeResetCount() {
+ synchronized (mAudioModeResetLock) {
+ mAudioModeResetCount = 0;
+ mAudioModeResetLock.notify();
+ }
+ }
+
/** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */
public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId,
AudioAttributes aa, String callingPackageName) {
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 e7e519e..e0913cc 100644
--- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
+++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java
@@ -28,6 +28,7 @@
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.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
@@ -73,6 +74,7 @@
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.UserHandle;
+import android.provider.Settings;
import android.util.ArrayMap;
import android.util.Slog;
import android.view.ContentRecordingSession;
@@ -195,6 +197,15 @@
if (mProjectionGrant == null || mProjectionGrant.packageName == null) {
return false;
}
+ boolean disableScreenShareProtections = Settings.Global.getInt(
+ getContext().getContentResolver(),
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0) != 0;
+ if (disableScreenShareProtections) {
+ Slog.v(TAG,
+ "Allowing keyguard capture as screenshare protections are disabled.");
+ return true;
+ }
+
if (mPackageManager.checkPermission(RECORD_SENSITIVE_CONTENT,
mProjectionGrant.packageName)
== PackageManager.PERMISSION_GRANTED) {
@@ -226,7 +237,8 @@
void onKeyguardLockedStateChanged(boolean isKeyguardLocked) {
if (!isKeyguardLocked) return;
synchronized (mLock) {
- if (mProjectionGrant != null && !canCaptureKeyguard()) {
+ if (mProjectionGrant != null && !canCaptureKeyguard()
+ && mProjectionGrant.mVirtualDisplayId != INVALID_DISPLAY) {
Slog.d(TAG, "Content Recording: Stopped MediaProjection"
+ " due to keyguard lock");
mProjectionGrant.stop();
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 425bb15..a89350c 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
@@ -25,6 +25,7 @@
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.provider.Settings.Global.DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS;
import static android.view.ContentRecordingSession.TARGET_UID_FULL_SCREEN;
import static android.view.ContentRecordingSession.TARGET_UID_UNKNOWN;
import static android.view.ContentRecordingSession.createDisplaySession;
@@ -80,6 +81,7 @@
import android.platform.test.annotations.EnableFlags;
import android.platform.test.annotations.Presubmit;
import android.platform.test.flag.junit.SetFlagsRule;
+import android.provider.Settings;
import android.testing.TestableContext;
import android.view.ContentRecordingSession;
import android.view.ContentRecordingSession.RecordContent;
@@ -372,6 +374,50 @@
});
}
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
+ public void testCreateProjection_keyguardLocked_screenshareProtectionsDisabled()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ int value = Settings.Global.getInt(mContext.getContentResolver(),
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 0);
+ try {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, 1);
+ doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+
+ doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+ RECORD_SENSITIVE_CONTENT, projection.packageName);
+
+ projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
+
+ // The projection was started because it was allowed to capture the keyguard.
+ assertThat(mService.getActiveProjectionInfo()).isNotNull();
+ } finally {
+ Settings.Global.putInt(mContext.getContentResolver(),
+ DISABLE_SCREEN_SHARE_PROTECTIONS_FOR_APPS_AND_NOTIFICATIONS, value);
+ }
+ }
+
+ @EnableFlags(android.companion.virtualdevice.flags
+ .Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS)
+ @Test
+ public void testCreateProjection_keyguardLocked_noDisplayCreated()
+ throws NameNotFoundException {
+ MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions();
+ doReturn(true).when(mKeyguardManager).isKeyguardLocked();
+
+ doReturn(PackageManager.PERMISSION_DENIED).when(mPackageManager).checkPermission(
+ RECORD_SENSITIVE_CONTENT, projection.packageName);
+
+ projection.start(mIMediaProjectionCallback);
+
+ // The projection was started because it was allowed to capture the keyguard.
+ assertThat(mService.getActiveProjectionInfo()).isNotNull();
+ }
+
@Test
public void testCreateProjection_attemptReuse_noPriorProjectionGrant()
throws NameNotFoundException {
@@ -485,6 +531,7 @@
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(service);
projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
assertThat(service.getActiveProjectionInfo()).isNotNull();
@@ -507,6 +554,7 @@
MediaProjectionManagerService.MediaProjection projection =
startProjectionPreconditions(service);
projection.start(mIMediaProjectionCallback);
+ projection.notifyVirtualDisplayCreated(10);
assertThat(service.getActiveProjectionInfo()).isNotNull();