Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7543cedb authored by Vadim Caen's avatar Vadim Caen Committed by Android (Google) Code Review
Browse files

Merge changes from topic "media-proj-builder" into main

* changes:
  Introduce MediaProjectionAppContent
  Builder pattern for MediaProjectionConfig
parents a1a7381a a97ff8e4
Loading
Loading
Loading
Loading
+22 −0
Original line number Diff line number Diff line
@@ -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 {
+19 −0
Original line number Diff line number Diff line
/*
 * 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
+123 −0
Original line number Diff line number Diff line
/*
 * 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];
                }
            };
}
+329 −25
Original line number Diff line number Diff line
@@ -20,22 +20,55 @@ 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.
     *
@@ -43,6 +76,12 @@ 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.
     *
@@ -50,33 +89,97 @@ public final class MediaProjectionConfig implements Parcelable {
     */
    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() {
        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() {
        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,28 +281,58 @@ 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;
        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;
        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);
        if (Flags.appContentSharing()) {
            dest.writeInt(mProjectionSources);
            dest.writeString(mRequesterHint);
            dest.writeInt(mInitialSelection);
        } else {
            dest.writeInt(mRegionToCapture);
        }
    }

    @Override
    public int describeContents() {
@@ -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);
        }
    }
}
+7 −3

File changed.

Preview size limit exceeded, changes collapsed.

Loading