diff options
author | 2025-03-21 12:15:43 -0700 | |
---|---|---|
committer | 2025-03-21 12:15:43 -0700 | |
commit | 7543cedb9f9abd61475e055b2db21615005c9c26 (patch) | |
tree | 72360e47c67c09519694f8ebd28c1eadf060039d | |
parent | a1a7381a9c4d65c18fd29f44bda9f05a7d6eaefb (diff) | |
parent | a97ff8e4a2102603d8c41a5989e8eb91e69a4fc1 (diff) |
Merge changes from topic "media-proj-builder" into main
* changes:
Introduce MediaProjectionAppContent
Builder pattern for MediaProjectionConfig
10 files changed, 639 insertions, 35 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 6416e07b17f3..4a73bf857948 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -27222,12 +27222,34 @@ package android.media.projection { method public void onStop(); } + @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public final class MediaProjectionAppContent implements android.os.Parcelable { + ctor public MediaProjectionAppContent(@NonNull android.graphics.Bitmap, @NonNull CharSequence, int); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionAppContent> CREATOR; + } + public final class MediaProjectionConfig implements android.os.Parcelable { method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForDefaultDisplay(); method @NonNull public static android.media.projection.MediaProjectionConfig createConfigForUserChoice(); method public int describeContents(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getInitiallySelectedSource(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public int getProjectionSources(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") @Nullable public CharSequence getRequesterHint(); + method @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public boolean isSourceEnabled(int); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.media.projection.MediaProjectionConfig> CREATOR; + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP = 8; // 0x8 + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_APP_CONTENT = 16; // 0x10 + field @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final int PROJECTION_SOURCE_DISPLAY = 2; // 0x2 + } + + @FlaggedApi("com.android.media.projection.flags.app_content_sharing") public static final class MediaProjectionConfig.Builder { + ctor public MediaProjectionConfig.Builder(); + method @NonNull public android.media.projection.MediaProjectionConfig build(); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setInitiallySelectedSource(int); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setRequesterHint(@Nullable String); + method @NonNull public android.media.projection.MediaProjectionConfig.Builder setSourceEnabled(int, boolean); } public final class MediaProjectionManager { diff --git a/media/java/android/media/projection/MediaProjectionAppContent.aidl b/media/java/android/media/projection/MediaProjectionAppContent.aidl new file mode 100644 index 000000000000..6ead69b9fdc6 --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionAppContent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright 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, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +parcelable MediaProjectionAppContent;
\ No newline at end of file diff --git a/media/java/android/media/projection/MediaProjectionAppContent.java b/media/java/android/media/projection/MediaProjectionAppContent.java new file mode 100644 index 000000000000..da0bdc191c0c --- /dev/null +++ b/media/java/android/media/projection/MediaProjectionAppContent.java @@ -0,0 +1,123 @@ +/* + * Copyright 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, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import android.annotation.FlaggedApi; +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +/** + * Holds information about content an app can share via the MediaProjection APIs. + * <p> + * An application requesting a {@link MediaProjection session} can add its own content in the + * list of available content along with the whole screen or a single application. + * <p> + * Each instance of {@link MediaProjectionAppContent} contains an id that is used to identify the + * content chosen by the user back to the advertising application, thus the meaning of the id is + * only relevant to that application. + */ +@FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING) +public final class MediaProjectionAppContent implements Parcelable { + + private final Bitmap mThumbnail; + private final CharSequence mTitle; + private final int mId; + + /** + * Constructor to pass a thumbnail, title and id. + * + * @param thumbnail The thumbnail representing this content to be shown to the user. + * @param title A user visible string representing the title of this content. + * @param id An arbitrary int defined by the advertising application to be fed back once + * the user made their choice. + */ + public MediaProjectionAppContent(@NonNull Bitmap thumbnail, @NonNull CharSequence title, + int id) { + mThumbnail = Objects.requireNonNull(thumbnail, "thumbnail can't be null").asShared(); + mTitle = Objects.requireNonNull(title, "title can't be null"); + mId = id; + } + + /** + * Returns thumbnail representing this content to be shown to the user. + * + * @hide + */ + @NonNull + public Bitmap getThumbnail() { + return mThumbnail; + } + + /** + * Returns user visible string representing the title of this content. + * + * @hide + */ + @NonNull + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns the arbitrary int defined by the advertising application to be fed back once + * the user made their choice. + * + * @hide + */ + public int getId() { + return mId; + } + + private MediaProjectionAppContent(Parcel in) { + mThumbnail = in.readParcelable(this.getClass().getClassLoader(), Bitmap.class); + mTitle = in.readCharSequence(); + mId = in.readInt(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeParcelable(mThumbnail, flags); + dest.writeCharSequence(mTitle); + dest.writeInt(mId); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator<MediaProjectionAppContent> CREATOR = + new Creator<>() { + @NonNull + @Override + public MediaProjectionAppContent createFromParcel(@NonNull Parcel in) { + return new MediaProjectionAppContent(in); + } + + @NonNull + @Override + public MediaProjectionAppContent[] newArray(int size) { + return new MediaProjectionAppContent[size]; + } + }; +} diff --git a/media/java/android/media/projection/MediaProjectionConfig.java b/media/java/android/media/projection/MediaProjectionConfig.java index 598b534e81ca..cd674e9f2ad1 100644 --- a/media/java/android/media/projection/MediaProjectionConfig.java +++ b/media/java/android/media/projection/MediaProjectionConfig.java @@ -20,23 +20,56 @@ import static android.view.Display.DEFAULT_DISPLAY; import static java.lang.annotation.RetentionPolicy.SOURCE; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.os.Parcelable; -import com.android.internal.util.AnnotationValidations; +import com.android.media.projection.flags.Flags; import java.lang.annotation.Retention; +import java.util.Arrays; +import java.util.Objects; /** * Configure the {@link MediaProjection} session requested from * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}. + * <p> + * This configuration should be used to provide the user with options for choosing the content to + * be shared with the requesting application. */ public final class MediaProjectionConfig implements Parcelable { /** + * Bitmask for setting whether this configuration is for projecting the whole display. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_DISPLAY = 1 << 1; + + /** + * Bitmask for setting whether this configuration is for projecting the a custom region display. + * + * @hide + */ + public static final int PROJECTION_SOURCE_DISPLAY_REGION = 1 << 2; + + /** + * Bitmask for setting whether this configuration is for projecting the a single application. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_APP = 1 << 3; + + /** + * Bitmask for setting whether this configuration is for projecting the content provided by an + * application. + */ + @FlaggedApi(com.android.media.projection.flags.Flags.FLAG_APP_CONTENT_SHARING) + public static final int PROJECTION_SOURCE_APP_CONTENT = 1 << 4; + + /** * The user, rather than the host app, determines which region of the display to capture. * * @hide @@ -44,39 +77,109 @@ public final class MediaProjectionConfig implements Parcelable { public static final int CAPTURE_REGION_USER_CHOICE = 0; /** + * @hide + */ + public static final int DEFAULT_PROJECTION_SOURCES = + PROJECTION_SOURCE_DISPLAY | PROJECTION_SOURCE_APP; + + /** * The host app specifies a particular display to capture. * * @hide */ public static final int CAPTURE_REGION_FIXED_DISPLAY = 1; + private static final int[] PROJECTION_SOURCES = + new int[]{PROJECTION_SOURCE_DISPLAY, PROJECTION_SOURCE_DISPLAY_REGION, + PROJECTION_SOURCE_APP, + PROJECTION_SOURCE_APP_CONTENT}; + + private static final String[] PROJECTION_SOURCES_STRING = + new String[]{"PROJECTION_SOURCE_DISPLAY", "PROJECTION_SOURCE_DISPLAY_REGION", + "PROJECTION_SOURCE_APP", "PROJECTION_SOURCE_APP_CONTENT"}; + + private static final int VALID_PROJECTION_SOURCES = createValidSourcesMask(); + + private final int mInitialSelection; + /** @hide */ @IntDef(prefix = "CAPTURE_REGION_", value = {CAPTURE_REGION_USER_CHOICE, CAPTURE_REGION_FIXED_DISPLAY}) @Retention(SOURCE) + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed public @interface CaptureRegion { } + /** @hide */ + @IntDef(flag = true, prefix = "PROJECTION_SOURCE_", value = {PROJECTION_SOURCE_DISPLAY, + PROJECTION_SOURCE_DISPLAY_REGION, PROJECTION_SOURCE_APP, PROJECTION_SOURCE_APP_CONTENT}) + @Retention(SOURCE) + public @interface MediaProjectionSource { + } + /** - * The particular display to capture. Only used when {@link #getRegionToCapture()} is - * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is set, + * ignored otherwise. * <p> * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. */ @IntRange(from = DEFAULT_DISPLAY, to = DEFAULT_DISPLAY) - private int mDisplayToCapture; + private final int mDisplayToCapture; /** * The region to capture. Defaults to the user's choice. */ @CaptureRegion + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private int mRegionToCapture; /** + * The region to capture. Defaults to the user's choice. + */ + @MediaProjectionSource + private final int mProjectionSources; + + /** + * @see #getRequesterHint() + */ + @Nullable + private final String mRequesterHint; + + /** * Customized instance, with region set to the provided value. + * @deprecated To be removed FLAG_APP_CONTENT_SHARING is removed */ + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private MediaProjectionConfig(@CaptureRegion int captureRegion) { + if (Flags.appContentSharing()) { + throw new UnsupportedOperationException( + "Flag FLAG_APP_CONTENT_SHARING enabled. This method must not be called."); + } mRegionToCapture = captureRegion; + mDisplayToCapture = DEFAULT_DISPLAY; + + mRequesterHint = null; + mInitialSelection = -1; + mProjectionSources = -1; + } + + /** + * Customized instance, with region set to the provided value. + */ + private MediaProjectionConfig(@MediaProjectionSource int projectionSource, + @Nullable String requesterHint, int displayId, int initialSelection) { + if (!Flags.appContentSharing()) { + throw new UnsupportedOperationException( + "Flag FLAG_APP_CONTENT_SHARING disabled. This method must not be called"); + } + if (projectionSource == 0) { + mProjectionSources = DEFAULT_PROJECTION_SOURCES; + } else { + mProjectionSources = projectionSource; + } + mRequesterHint = requesterHint; + mDisplayToCapture = displayId; + mInitialSelection = initialSelection; } /** @@ -84,16 +187,17 @@ public final class MediaProjectionConfig implements Parcelable { */ @NonNull public static MediaProjectionConfig createConfigForDefaultDisplay() { - MediaProjectionConfig config = new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY); - config.mDisplayToCapture = DEFAULT_DISPLAY; - return config; + if (Flags.appContentSharing()) { + return new Builder().setSourceEnabled(PROJECTION_SOURCE_DISPLAY, true).build(); + } else { + return new MediaProjectionConfig(CAPTURE_REGION_FIXED_DISPLAY); + } } /** * Returns an instance which allows the user to decide which region is captured. The consent * dialog presents the user with all possible options. If the user selects display capture, * then only the {@link android.view.Display#DEFAULT_DISPLAY} is supported. - * * <p> * When passed in to * {@link MediaProjectionManager#createScreenCaptureIntent(MediaProjectionConfig)}, the consent @@ -103,13 +207,18 @@ public final class MediaProjectionConfig implements Parcelable { */ @NonNull public static MediaProjectionConfig createConfigForUserChoice() { - return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE); + if (Flags.appContentSharing()) { + return new MediaProjectionConfig.Builder().build(); + } else { + return new MediaProjectionConfig(CAPTURE_REGION_USER_CHOICE); + } } /** * Returns string representation of the captured region. */ @NonNull + @Deprecated // Remove when FLAG_APP_CONTENT_SHARING is removed private static String captureRegionToString(int value) { return switch (value) { case CAPTURE_REGION_USER_CHOICE -> "CAPTURE_REGION_USERS_CHOICE"; @@ -118,16 +227,42 @@ public final class MediaProjectionConfig implements Parcelable { }; } + /** + * Returns string representation of the captured region. + */ + @NonNull + private static String projectionSourceToString(int value) { + StringBuilder stringBuilder = new StringBuilder(); + for (int i = 0; i < PROJECTION_SOURCES.length; i++) { + if ((value & PROJECTION_SOURCES[i]) > 0) { + stringBuilder.append(PROJECTION_SOURCES_STRING[i]); + stringBuilder.append(" "); + value &= ~PROJECTION_SOURCES[i]; + } + } + if (value > 0) { + stringBuilder.append("Unknown projection sources: "); + stringBuilder.append(Integer.toHexString(value)); + } + return stringBuilder.toString(); + } + @Override public String toString() { - return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", " - + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }"; + if (Flags.appContentSharing()) { + return ("MediaProjectionConfig{mInitialSelection=%d, mDisplayToCapture=%d, " + + "mProjectionSource=%s, mRequesterHint='%s'}").formatted(mInitialSelection, + mDisplayToCapture, projectionSourceToString(mProjectionSources), + mRequesterHint); + } else { + return "MediaProjectionConfig { " + "displayToCapture = " + mDisplayToCapture + ", " + + "regionToCapture = " + captureRegionToString(mRegionToCapture) + " }"; + } } - /** - * The particular display to capture. Only used when {@link #getRegionToCapture()} is - * {@link #CAPTURE_REGION_FIXED_DISPLAY}; ignored otherwise. + * The particular display to capture. Only used when {@link #PROJECTION_SOURCE_DISPLAY} is + * set; ignored otherwise. * <p> * Only supports values of {@link android.view.Display#DEFAULT_DISPLAY}. * @@ -146,27 +281,57 @@ public final class MediaProjectionConfig implements Parcelable { return mRegionToCapture; } + /** + * A bitmask representing of requested projection sources. + * <p> + * The system supports different kind of media projection session. Although the user is + * picking the target content, the requesting application can configure the choices displayed + * to the user. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public @MediaProjectionSource int getProjectionSources() { + return mProjectionSources; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MediaProjectionConfig that = (MediaProjectionConfig) o; - return mDisplayToCapture == that.mDisplayToCapture - && mRegionToCapture == that.mRegionToCapture; + if (Flags.appContentSharing()) { + return mDisplayToCapture == that.mDisplayToCapture + && mProjectionSources == that.mProjectionSources + && mInitialSelection == that.mInitialSelection + && Objects.equals(mRequesterHint, that.mRequesterHint); + } else { + return mDisplayToCapture == that.mDisplayToCapture + && mRegionToCapture == that.mRegionToCapture; + } } @Override public int hashCode() { int _hash = 1; - _hash = 31 * _hash + mDisplayToCapture; - _hash = 31 * _hash + mRegionToCapture; + if (Flags.appContentSharing()) { + return Objects.hash(mDisplayToCapture, mProjectionSources, mInitialSelection, + mRequesterHint); + } else { + _hash = 31 * _hash + mDisplayToCapture; + _hash = 31 * _hash + mRegionToCapture; + } return _hash; } @Override public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { dest.writeInt(mDisplayToCapture); - dest.writeInt(mRegionToCapture); + if (Flags.appContentSharing()) { + dest.writeInt(mProjectionSources); + dest.writeString(mRequesterHint); + dest.writeInt(mInitialSelection); + } else { + dest.writeInt(mRegionToCapture); + } } @Override @@ -176,12 +341,17 @@ public final class MediaProjectionConfig implements Parcelable { /** @hide */ /* package-private */ MediaProjectionConfig(@NonNull android.os.Parcel in) { - int displayToCapture = in.readInt(); - int regionToCapture = in.readInt(); - - mDisplayToCapture = displayToCapture; - mRegionToCapture = regionToCapture; - AnnotationValidations.validate(CaptureRegion.class, null, mRegionToCapture); + mDisplayToCapture = in.readInt(); + if (Flags.appContentSharing()) { + mProjectionSources = in.readInt(); + mRequesterHint = in.readString(); + mInitialSelection = in.readInt(); + } else { + mRegionToCapture = in.readInt(); + mProjectionSources = -1; + mRequesterHint = null; + mInitialSelection = -1; + } } public static final @NonNull Parcelable.Creator<MediaProjectionConfig> CREATOR = @@ -196,4 +366,138 @@ public final class MediaProjectionConfig implements Parcelable { return new MediaProjectionConfig(in); } }; + + /** + * Returns true if the provided source should be enabled. + * + * @param projectionSource projection source integer to check for. The parameter can also be a + * bitmask of multiple sources. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public boolean isSourceEnabled(@MediaProjectionSource int projectionSource) { + return (mProjectionSources & projectionSource) > 0; + } + + /** + * Returns a bit mask of one, and only one, of the projection type flag. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + @MediaProjectionSource + public int getInitiallySelectedSource() { + return mInitialSelection; + } + + /** + * A hint set by the requesting app indicating who the requester of this {@link MediaProjection} + * session is. + * <p> + * The UI component prompting the user for the permission to start the session can use + * this hint to provide more information about the origin of the request (e.g. a browser + * tab title, a meeting id if sharing to a video conferencing app, a player name if + * sharing the screen within a game). + * + * @return the hint to be displayed if set, null otherwise. + */ + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + @Nullable + public CharSequence getRequesterHint() { + return mRequesterHint; + } + + private static int createValidSourcesMask() { + int validSources = 0; + for (int projectionSource : PROJECTION_SOURCES) { + validSources |= projectionSource; + } + return validSources; + } + + @FlaggedApi(Flags.FLAG_APP_CONTENT_SHARING) + public static final class Builder { + private int mOptions = 0; + private String mRequesterHint = null; + + @MediaProjectionSource + private int mInitialSelection; + + public Builder() { + if (!Flags.appContentSharing()) { + throw new UnsupportedOperationException("Flag FLAG_APP_CONTENT_SHARING disabled"); + } + } + + /** + * Indicates which projection source the UI component should display to the user + * first. Calling this method without enabling the respective choice will have no effect. + * + * @return instance of this {@link Builder}. + * @see #setSourceEnabled(int, boolean) + */ + @NonNull + public Builder setInitiallySelectedSource(@MediaProjectionSource int projectionSource) { + for (int source : PROJECTION_SOURCES) { + if (projectionSource == source) { + mInitialSelection = projectionSource; + return this; + } + } + throw new IllegalArgumentException( + ("projectionSource is no a valid projection source. projectionSource must be " + + "one of %s but was %s") + .formatted(Arrays.toString(PROJECTION_SOURCES_STRING), + projectionSourceToString(projectionSource))); + } + + /** + * Let the requesting app indicate who the requester of this {@link MediaProjection} + * session is.. + * <p> + * The UI component prompting the user for the permission to start the session can use + * this hint to provide more information about the origin of the request (e.g. a browser + * tab title, a meeting id if sharing to a video conferencing app, a player name if + * sharing the screen within a game). + * <p> + * Note that setting this won't hide or change the name of the application + * requesting the session. + * + * @return instance of this {@link Builder}. + */ + @NonNull + public Builder setRequesterHint(@Nullable String requesterHint) { + mRequesterHint = requesterHint; + return this; + } + + /** + * Set whether the UI component requesting the user permission to share their screen + * should display an option to share the specified source + * + * @param source the projection source to enable or disable + * @param enabled true to enable the source, false otherwise + * @return this instance for chaining. + * @throws IllegalArgumentException if the source is not one of the valid sources. + */ + @NonNull + @SuppressLint("MissingGetterMatchingBuilder") // isSourceEnabled is defined + public Builder setSourceEnabled(@MediaProjectionSource int source, boolean enabled) { + if ((source & VALID_PROJECTION_SOURCES) == 0) { + throw new IllegalArgumentException( + ("source is no a valid projection source. source must be " + + "any of %s but was %s") + .formatted(Arrays.toString(PROJECTION_SOURCES_STRING), + projectionSourceToString(source))); + } + mOptions = enabled ? mOptions | source : mOptions & ~source; + return this; + } + + /** + * Builds a new immutable instance of {@link MediaProjectionConfig} + */ + @NonNull + public MediaProjectionConfig build() { + return new MediaProjectionConfig(mOptions, mRequesterHint, DEFAULT_DISPLAY, + mInitialSelection); + } + } } diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9036bf385d96..4a5392d3c0c3 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -29,6 +29,7 @@ import android.compat.annotation.Overridable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.hardware.display.VirtualDisplay; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; @@ -78,9 +79,12 @@ public final class MediaProjectionManager { private static final String TAG = "MediaProjectionManager"; /** - * This change id ensures that users are presented with a choice of capturing a single app - * or the entire screen when initiating a MediaProjection session, overriding the usage of - * MediaProjectionConfig#createConfigForDefaultDisplay. + * If enabled, this change id ensures that users are presented with a choice of capturing a + * single app and the entire screen when initiating a MediaProjection session, overriding the + * usage of MediaProjectionConfig#createConfigForDefaultDisplay. + * <p> + * + * <a href=" https://developer.android.com/guide/practices/device-compatibility-mode#override_disable_media_projection_single_app_option">More info</a> * * @hide */ diff --git a/media/java/android/media/projection/TEST_MAPPING b/media/java/android/media/projection/TEST_MAPPING index ea62287b7411..62e776b822d2 100644 --- a/media/java/android/media/projection/TEST_MAPPING +++ b/media/java/android/media/projection/TEST_MAPPING @@ -4,4 +4,4 @@ "path": "frameworks/base/services/core/java/com/android/server/media/projection" } ] -}
\ No newline at end of file +} diff --git a/media/tests/projection/Android.bp b/media/tests/projection/Android.bp index 0b02d3cb4250..0b4b7dbbca1f 100644 --- a/media/tests/projection/Android.bp +++ b/media/tests/projection/Android.bp @@ -26,6 +26,7 @@ android_test { "androidx.test.runner", "androidx.test.rules", "androidx.test.ext.junit", + "flag-junit", "frameworks-base-testutils", "mockito-target-extended-minus-junit4", "platform-test-annotations", diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java new file mode 100644 index 000000000000..7e167c63a2a2 --- /dev/null +++ b/media/tests/projection/src/android/media/projection/MediaProjectionAppContentTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 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, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.projection; + +import static com.google.common.truth.Truth.assertThat; + +import android.graphics.Bitmap; +import android.os.Parcel; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MediaProjectionAppContentTest { + + @Test + public void testConstructorAndGetters() { + // Create a mock Bitmap + Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + // Create a MediaProjectionAppContent object + MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title", + 123); + + // Verify the values using getters + assertThat(content.getTitle()).isEqualTo("Test Title"); + assertThat(content.getId()).isEqualTo(123); + // Compare bitmap configurations and dimensions + assertThat(content.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig()); + assertThat(content.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth()); + assertThat(content.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight()); + } + + @Test + public void testParcelable() { + // Create a mock Bitmap + Bitmap mockBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888); + + // Create a MediaProjectionAppContent object + MediaProjectionAppContent content = new MediaProjectionAppContent(mockBitmap, "Test Title", + 123); + + // Parcel and unparcel the object + Parcel parcel = Parcel.obtain(); + content.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + MediaProjectionAppContent unparceledContent = + MediaProjectionAppContent.CREATOR.createFromParcel(parcel); + + // Verify the values of the unparceled object + assertThat(unparceledContent.getTitle()).isEqualTo("Test Title"); + assertThat(unparceledContent.getId()).isEqualTo(123); + // Compare bitmap configurations and dimensions + assertThat(unparceledContent.getThumbnail().getConfig()).isEqualTo(mockBitmap.getConfig()); + assertThat(unparceledContent.getThumbnail().getWidth()).isEqualTo(mockBitmap.getWidth()); + assertThat(unparceledContent.getThumbnail().getHeight()).isEqualTo(mockBitmap.getHeight()); + + parcel.recycle(); + } + + @Test + public void testCreatorNewArray() { + // Create a new array using the CREATOR + MediaProjectionAppContent[] contentArray = MediaProjectionAppContent.CREATOR.newArray(5); + + // Verify that the array is not null and has the correct size + assertThat(contentArray).isNotNull(); + assertThat(contentArray).hasLength(5); + } +} diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java index 2820606958b7..bc0eae1a3ec7 100644 --- a/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java +++ b/media/tests/projection/src/android/media/projection/MediaProjectionConfigTest.java @@ -18,22 +18,31 @@ package android.media.projection; import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY; import static android.media.projection.MediaProjectionConfig.CAPTURE_REGION_USER_CHOICE; +import static android.media.projection.MediaProjectionConfig.PROJECTION_SOURCE_DISPLAY; +import static android.media.projection.MediaProjectionConfig.DEFAULT_PROJECTION_SOURCES; import static android.view.Display.DEFAULT_DISPLAY; import static com.google.common.truth.Truth.assertThat; import android.os.Parcel; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.media.projection.flags.Flags; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; /** * Tests for the {@link MediaProjectionConfig} class. - * + * <p> * Build/Install/Run: * atest MediaProjectionTests:MediaProjectionConfigTest */ @@ -41,6 +50,11 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class MediaProjectionConfigTest { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final MediaProjectionConfig DISPLAY_CONFIG = MediaProjectionConfig.createConfigForDefaultDisplay(); private static final MediaProjectionConfig USERS_CHOICE_CONFIG = @@ -57,17 +71,33 @@ public class MediaProjectionConfigTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING) public void testCreateDisplayConfig() { assertThat(DISPLAY_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_FIXED_DISPLAY); assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY); } @Test + @RequiresFlagsDisabled(Flags.FLAG_APP_CONTENT_SHARING) public void testCreateUsersChoiceConfig() { assertThat(USERS_CHOICE_CONFIG.getRegionToCapture()).isEqualTo(CAPTURE_REGION_USER_CHOICE); } @Test + @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING) + public void testDefaultProjectionSources() { + assertThat(USERS_CHOICE_CONFIG.getProjectionSources()) + .isEqualTo(DEFAULT_PROJECTION_SOURCES); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_APP_CONTENT_SHARING) + public void testCreateDisplayConfigProjectionSource() { + assertThat(DISPLAY_CONFIG.getProjectionSources()).isEqualTo(PROJECTION_SOURCE_DISPLAY); + assertThat(DISPLAY_CONFIG.getDisplayToCapture()).isEqualTo(DEFAULT_DISPLAY); + } + + @Test public void testEquals() { assertThat(MediaProjectionConfig.createConfigForUserChoice()).isEqualTo( USERS_CHOICE_CONFIG); diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt index 88cbc3867744..a8d0e0573d89 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionUtils.kt @@ -18,6 +18,7 @@ package com.android.systemui.mediaprojection.permission import android.content.Context import android.media.projection.MediaProjectionConfig +import com.android.media.projection.flags.Flags import com.android.systemui.res.R /** Various utility methods related to media projection permissions. */ @@ -28,13 +29,27 @@ object MediaProjectionPermissionUtils { mediaProjectionConfig: MediaProjectionConfig?, overrideDisableSingleAppOption: Boolean, ): String? { - // The single app option should only be disabled if the client has setup a - // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND - // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app override. + val singleAppOptionDisabled = !overrideDisableSingleAppOption && - mediaProjectionConfig?.regionToCapture == - MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY + if (Flags.appContentSharing()) { + // The single app option should only be disabled if the client has setup a + // MediaProjection with MediaProjection.isChoiceAppEnabled == false (e.g by + // creating it + // with MediaProjectionConfig#createConfigForDefaultDisplay AND + // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app + // override. + mediaProjectionConfig?.isSourceEnabled( + MediaProjectionConfig.PROJECTION_SOURCE_APP + ) == false + } else { + // The single app option should only be disabled if the client has setup a + // MediaProjection with MediaProjectionConfig#createConfigForDefaultDisplay AND + // it hasn't been overridden by the OVERRIDE_DISABLE_SINGLE_APP_OPTION per-app + // override. + mediaProjectionConfig?.regionToCapture == + MediaProjectionConfig.CAPTURE_REGION_FIXED_DISPLAY + } return if (singleAppOptionDisabled) { context.getString( R.string.media_projection_entry_app_permission_dialog_single_app_disabled, |