diff options
9 files changed, 280 insertions, 3 deletions
diff --git a/media/java/android/media/flags/projection.aconfig b/media/java/android/media/flags/projection.aconfig index fa1349c61c4c..6d4f0b4f47d5 100644 --- a/media/java/android/media/flags/projection.aconfig +++ b/media/java/android/media/flags/projection.aconfig @@ -29,3 +29,13 @@ flag { is_exported: true } +flag { + namespace: "media_projection" + name: "show_stop_dialog_post_call_end" + description: "Shows a stop dialog for MediaProjection sessions that started during call and remain active after a call ends" + bug: "390343524" + metadata { + purpose: PURPOSE_BUGFIX + } + is_exported: true +} diff --git a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl index e46d34e81483..3baf4d7efd65 100644 --- a/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl +++ b/media/java/android/media/projection/IMediaProjectionWatcherCallback.aidl @@ -18,6 +18,7 @@ package android.media.projection; import android.media.projection.MediaProjectionInfo; import android.view.ContentRecordingSession; +import android.media.projection.MediaProjectionEvent; /** {@hide} */ oneway interface IMediaProjectionWatcherCallback { @@ -35,4 +36,19 @@ oneway interface IMediaProjectionWatcherCallback { in MediaProjectionInfo info, in @nullable ContentRecordingSession session ); + + /** + * Called when a specific {@link MediaProjectionEvent} occurs during the media projection session. + * + * @param event contains the event type, which describes the nature/context of the event. + * @param info optional {@link MediaProjectionInfo} containing details about the media + projection host. + * @param session the recording session for the current media projection. Can be + * {@code null} when the recording will stop. + */ + void onMediaProjectionEvent( + in MediaProjectionEvent event, + in @nullable MediaProjectionInfo info, + in @nullable ContentRecordingSession session + ); } diff --git a/media/java/android/media/projection/MediaProjectionEvent.aidl b/media/java/android/media/projection/MediaProjectionEvent.aidl new file mode 100644 index 000000000000..34359900ce81 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionEvent.aidl @@ -0,0 +1,3 @@ +package android.media.projection; + +parcelable MediaProjectionEvent;
\ No newline at end of file diff --git a/media/java/android/media/projection/MediaProjectionEvent.java b/media/java/android/media/projection/MediaProjectionEvent.java new file mode 100644 index 000000000000..6922560c8abe --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionEvent.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 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, + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** @hide */ +public final class MediaProjectionEvent implements Parcelable { + + /** + * Represents various media projection events. + */ + @IntDef({PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL}) + @Retention(RetentionPolicy.SOURCE) + public @interface EventType {} + + /** Event type for when a call ends but the session is still active. */ + public static final int PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL = 0; + + private final @EventType int mEventType; + private final long mTimestampMillis; + + public MediaProjectionEvent(@EventType int eventType, long timestampMillis) { + mEventType = eventType; + mTimestampMillis = timestampMillis; + } + + private MediaProjectionEvent(Parcel in) { + mEventType = in.readInt(); + mTimestampMillis = in.readLong(); + } + + public @EventType int getEventType() { + return mEventType; + } + + public long getTimestampMillis() { + return mTimestampMillis; + } + + @Override + public boolean equals(Object o) { + if (o instanceof MediaProjectionEvent other) { + return mEventType == other.mEventType && mTimestampMillis == other.mTimestampMillis; + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(mEventType, mTimestampMillis); + } + + @Override + public String toString() { + return "MediaProjectionEvent{mEventType=" + mEventType + ", mTimestampMillis=" + + mTimestampMillis + "}"; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeInt(mEventType); + out.writeLong(mTimestampMillis); + } + + public static final Parcelable.Creator<MediaProjectionEvent> CREATOR = + new Parcelable.Creator<>() { + @Override + public MediaProjectionEvent createFromParcel(Parcel in) { + return new MediaProjectionEvent(in); + } + + @Override + public MediaProjectionEvent[] newArray(int size) { + return new MediaProjectionEvent[size]; + } + }; +} diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9cc2cca441a4..9036bf385d96 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -363,6 +363,19 @@ public final class MediaProjectionManager { @Nullable ContentRecordingSession session ) { } + + /** + * Called when a specific {@link MediaProjectionEvent} occurs during the media projection + * session. + * + * @param event the media projection event details. + * @param info optional details about the media projection host. + * @param session optional associated recording session details. + */ + public void onMediaProjectionEvent( + final MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable final ContentRecordingSession session) {} } /** @hide */ @@ -405,5 +418,13 @@ public final class MediaProjectionManager { ) { mHandler.post(() -> mCallback.onRecordingSessionSet(info, session)); } + + @Override + public void onMediaProjectionEvent( + final MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable final ContentRecordingSession session) { + mHandler.post(() -> mCallback.onMediaProjectionEvent(event, info, session)); + } } } 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 c428f39fd9d0..34a6cb951d46 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -58,6 +58,7 @@ import android.media.projection.IMediaProjection; import android.media.projection.IMediaProjectionCallback; import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionEvent; import android.media.projection.MediaProjectionInfo; import android.media.projection.MediaProjectionManager; import android.media.projection.ReviewGrantedConsentResult; @@ -80,6 +81,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; +import com.android.media.projection.flags.Flags; import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.Watchdog; @@ -177,9 +179,31 @@ public final class MediaProjectionManagerService extends SystemService private void maybeStopMediaProjection(int reason) { synchronized (mLock) { - if (!mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) { - Slog.d(TAG, "Content Recording: Stopping MediaProjection due to " - + MediaProjectionStopController.stopReasonToString(reason)); + if (mMediaProjectionStopController.isExemptFromStopping(mProjectionGrant, reason)) { + return; + } + + if (Flags.showStopDialogPostCallEnd() + && mMediaProjectionStopController.isStopReasonCallEnd(reason)) { + MediaProjectionEvent event = + new MediaProjectionEvent( + MediaProjectionEvent + .PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL, + System.currentTimeMillis()); + Slog.d( + TAG, + "Scheduling event: " + + event.getEventType() + + " for reason: " + + MediaProjectionStopController.stopReasonToString(reason)); + + // Post the PROJECTION_STARTED_DURING_CALL_AND_ACTIVE_POST_CALL event with a delay. + mHandler.postDelayed(() -> dispatchEvent(event), 500); + } else { + Slog.d( + TAG, + "Stopping MediaProjection due to reason: " + + MediaProjectionStopController.stopReasonToString(reason)); mProjectionGrant.stop(StopReason.STOP_DEVICE_LOCKED); } } @@ -388,6 +412,24 @@ public final class MediaProjectionManagerService extends SystemService mCallbackDelegate.dispatchSession(projectionInfo, session); } + private void dispatchEvent(@NonNull MediaProjectionEvent event) { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd " + + "is disabled. Event details: " + + event); + return; + } + MediaProjectionInfo projectionInfo; + ContentRecordingSession session; + synchronized (mLock) { + projectionInfo = mProjectionGrant != null ? mProjectionGrant.getProjectionInfo() : null; + session = mProjectionGrant != null ? mProjectionGrant.mSession : null; + } + mCallbackDelegate.dispatchEvent(event, projectionInfo, session); + } + /** * Returns {@code true} when updating the current mirroring session on WM succeeded, and * {@code false} otherwise. @@ -1467,6 +1509,25 @@ public final class MediaProjectionManagerService extends SystemService } } + private void dispatchEvent( + @NonNull MediaProjectionEvent event, + @Nullable MediaProjectionInfo info, + @Nullable ContentRecordingSession session) { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Event dispatch skipped. Reason: Flag showStopDialogPostCallEnd " + + "is disabled. Event details: " + + event); + return; + } + synchronized (mLock) { + for (IMediaProjectionWatcherCallback callback : mWatcherCallbacks.values()) { + mHandler.post(new WatcherEventCallback(callback, event, info, session)); + } + } + } + public void dispatchSession( @NonNull MediaProjectionInfo projectionInfo, @Nullable ContentRecordingSession session) { @@ -1593,6 +1654,41 @@ public final class MediaProjectionManagerService extends SystemService } } + private static final class WatcherEventCallback implements Runnable { + private final IMediaProjectionWatcherCallback mCallback; + private final MediaProjectionEvent mEvent; + private final MediaProjectionInfo mProjectionInfo; + private final ContentRecordingSession mSession; + + WatcherEventCallback( + @NonNull IMediaProjectionWatcherCallback callback, + @NonNull MediaProjectionEvent event, + @Nullable MediaProjectionInfo projectionInfo, + @Nullable ContentRecordingSession session) { + mCallback = callback; + mEvent = event; + mProjectionInfo = projectionInfo; + mSession = session; + } + + @Override + public void run() { + if (!Flags.showStopDialogPostCallEnd()) { + Slog.d( + TAG, + "Not running WatcherEventCallback. Reason: Flag " + + "showStopDialogPostCallEnd is disabled. " + ); + return; + } + try { + mCallback.onMediaProjectionEvent(mEvent, mProjectionInfo, mSession); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to notify MediaProjectionEvent change", e); + } + } + } + private static final class WatcherSessionCallback implements Runnable { private final IMediaProjectionWatcherCallback mCallback; private final MediaProjectionInfo mProjectionInfo; diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java index c018e6bc1dc7..2e0bb4f88485 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionStopController.java @@ -95,6 +95,11 @@ public class MediaProjectionStopController { } } + /** Checks if the given stop reason corresponds to a call ending. */ + public boolean isStopReasonCallEnd(int stopReason) { + return stopReason == STOP_REASON_CALL_END; + } + /** * Checks whether the given projection grant is exempt from stopping restrictions. */ diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index efc68aac0323..00e1c01bbadb 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -22,6 +22,7 @@ import static com.android.internal.protolog.WmProtoLogGroups.WM_ERROR; import android.media.projection.IMediaProjectionManager; import android.media.projection.IMediaProjectionWatcherCallback; +import android.media.projection.MediaProjectionEvent; import android.media.projection.MediaProjectionInfo; import android.os.Binder; import android.os.IBinder; @@ -84,6 +85,12 @@ public class ScreenRecordingCallbackController { public void onRecordingSessionSet(MediaProjectionInfo mediaProjectionInfo, ContentRecordingSession contentRecordingSession) { } + + @Override + public void onMediaProjectionEvent( + MediaProjectionEvent event, + MediaProjectionInfo mediaProjectionInfo, + ContentRecordingSession session) {} } ScreenRecordingCallbackController(WindowManagerService wms) { diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java index affcfc14034e..379079a0018c 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionStopControllerTest.java @@ -300,6 +300,22 @@ public class MediaProjectionStopControllerTest { } @Test + public void isStopReasonCallEnd_stopReasonCallEnd_returnsTrue() { + boolean result = + mStopController.isStopReasonCallEnd( + MediaProjectionStopController.STOP_REASON_CALL_END); + assertThat(result).isTrue(); + } + + @Test + public void isStopReasonCallEnd_stopReasonKeyguard_returnsFalse() { + boolean result = + mStopController.isStopReasonCallEnd( + MediaProjectionStopController.STOP_REASON_KEYGUARD); + assertThat(result).isFalse(); + } + + @Test @EnableFlags( android.companion.virtualdevice.flags.Flags.FLAG_MEDIA_PROJECTION_KEYGUARD_RESTRICTIONS) public void testKeyguardLockedStateChanged_unlocked() { |