diff options
15 files changed, 838 insertions, 14 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 77d39badeaaf..73d0d5baa6f7 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -53413,6 +53413,7 @@ package android.view { method @NonNull public android.view.SurfaceControl.Transaction setBuffer(@NonNull android.view.SurfaceControl, @Nullable android.hardware.HardwareBuffer, @Nullable android.hardware.SyncFence, @Nullable java.util.function.Consumer<android.hardware.SyncFence>); method @NonNull public android.view.SurfaceControl.Transaction setBufferSize(@NonNull android.view.SurfaceControl, @IntRange(from=0) int, @IntRange(from=0) int); method @NonNull public android.view.SurfaceControl.Transaction setBufferTransform(@NonNull android.view.SurfaceControl, int); + method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public android.view.SurfaceControl.Transaction setContentPriority(@NonNull android.view.SurfaceControl, int); method @NonNull public android.view.SurfaceControl.Transaction setCrop(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Rect); method @NonNull public android.view.SurfaceControl.Transaction setDamageRegion(@NonNull android.view.SurfaceControl, @Nullable android.graphics.Region); method @NonNull public android.view.SurfaceControl.Transaction setDataSpace(@NonNull android.view.SurfaceControl, int); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index c184ba54bdf7..a0d10f5f9751 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -8171,6 +8171,14 @@ package android.media.quality { method @NonNull public android.media.quality.PictureProfile.Builder setProfileType(int); } + @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public final class PictureProfileHandle implements android.os.Parcelable { + method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public int describeContents(); + method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public long getId(); + method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") public void writeToParcel(@NonNull android.os.Parcel, int); + field @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public static final android.os.Parcelable.Creator<android.media.quality.PictureProfileHandle> CREATOR; + field @NonNull public static final android.media.quality.PictureProfileHandle NONE; + } + } package android.media.session { @@ -18999,6 +19007,10 @@ package android.util { package android.view { + public static class SurfaceControl.Transaction implements java.io.Closeable android.os.Parcelable { + method @FlaggedApi("android.media.tv.flags.apply_picture_profiles") @NonNull public android.view.SurfaceControl.Transaction setPictureProfileHandle(@NonNull android.view.SurfaceControl, @NonNull android.media.quality.PictureProfileHandle); + } + @UiThread public class View implements android.view.accessibility.AccessibilityEventSource android.graphics.drawable.Drawable.Callback android.view.KeyEvent.Callback { method @NonNull public final java.util.List<android.graphics.Rect> getUnrestrictedPreferKeepClearRects(); method @RequiresPermission(android.Manifest.permission.SET_UNRESTRICTED_KEEP_CLEAR_AREAS) public final void setUnrestrictedPreferKeepClearRects(@NonNull java.util.List<android.graphics.Rect>); diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index 446d23e17903..68674dd402b4 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -37,6 +37,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.Size; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.compat.annotation.UnsupportedAppUsage; import android.graphics.ColorSpace; @@ -58,6 +59,7 @@ import android.hardware.display.DeviceProductInfo; import android.hardware.display.DisplayedContentSample; import android.hardware.display.DisplayedContentSamplingAttributes; import android.hardware.graphics.common.DisplayDecorationSupport; +import android.media.quality.PictureProfileHandle; import android.opengl.EGLDisplay; import android.opengl.EGLSync; import android.os.Build; @@ -234,7 +236,6 @@ public final class SurfaceControl implements Parcelable { long nativeObject, float currentBufferRatio, float desiredRatio); private static native void nativeSetDesiredHdrHeadroom(long transactionObj, long nativeObject, float desiredRatio); - private static native void nativeSetCachingHint(long transactionObj, long nativeObject, int cachingHint); private static native void nativeSetDamageRegion(long transactionObj, long nativeObject, @@ -314,6 +315,11 @@ public final class SurfaceControl implements Parcelable { private static native void nativeSetLuts(long transactionObj, long nativeObject, float[] buffers, int[] slots, int[] dimensions, int[] sizes, int[] samplingKeys); private static native void nativeEnableDebugLogCallPoints(long transactionObj); + private static native int nativeGetMaxPictureProfiles(); + private static native void nativeSetPictureProfileId(long transactionObj, + long nativeObject, long pictureProfileId); + private static native void nativeSetContentPriority(long transactionObj, long nativeObject, + int priority); /** * Transforms that can be applied to buffers as they are displayed to a window. @@ -2836,6 +2842,33 @@ public final class SurfaceControl implements Parcelable { } /** + * Retrieve the maximum number of concurrent picture profiles allowed across all displays. + * + * A picture profile is assigned to a layer via: + * <ul> + * <li>Picture processing via {@link MediaCodec.KEY_PICTURE_PROFILE}</li> + * <li>Picture processing via {@link SurfaceControl.Transaction#setPictureProfileHandle} + * </li> + * </ul> + * + * If the maximum number is exceeded, some layers will not receive profiles based on: + * <ul> + * <li>The content priority assigned by the app</li> + * <li>The system-determined priority of the app owning the layer</li> + * </ul> + * + * @see MediaCodec.KEY_PICTURE_PROFILE + * @see SurfaceControl.Transaction#setPictureProfileHandle + * @see SurfaceControl.Transaction#setContentPriority + * + * @hide + */ + @IntRange(from = 0) + public static int getMaxPictureProfiles() { + return nativeGetMaxPictureProfiles(); + } + + /** * Interface to handle request to * {@link SurfaceControl.Transaction#addTransactionCommittedListener(Executor, TransactionCommittedListener)} */ @@ -4598,6 +4631,71 @@ public final class SurfaceControl implements Parcelable { } /** + * Sets the desired picture profile handle for a layer. + * <p> + * A handle, retrieved from {@link MediaQualityManager#getProfileHandles}, which + * refers a set of parameters that are used to configure picture processing that is applied + * to all subsequent buffers to enhance the quality of their contents (e.g. gamma, color + * temperature, hue, saturation, etc.). + * <p> + * Setting a handle does not guarantee access to limited picture processing. The content + * priority for the as well as the prominance of app to the current user experience plays a + * role in which layer(s) get access to the limited picture processing resources. A maximum + * number of {@link MediaQualityManager.getMaxPictureProfiles} can be applied at any given + * point in time. + * + * @param sc The SurfaceControl of the layer that should be updated + * @param handle The picture profile handle which refers to the set of desired parameters + * + * @see MediaQualityManager#getMaxPictureProfiles + * @see MediaQualityManager#getProfileHandles + * @see MediaCodec.KEY_PICTURE_PROFILE + * @see SurfaceControl.Transaction#setContentPriority + * + * @hide + */ + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) + @SystemApi + public @NonNull Transaction setPictureProfileHandle(@NonNull SurfaceControl sc, + @NonNull PictureProfileHandle handle) { + checkPreconditions(sc); + + nativeSetPictureProfileId(mNativeObject, sc.mNativeObject, handle.getId()); + return this; + } + + /** + * Sets the importance the layer's contents has to the app's user experience. + * <p> + * When a two layers within the same app are competing for a limited rendering resource, + * the priority will determine which layer gets access to the resource. The lower the + * priority, the more likely the layer will get access to the resource. + * <p> + * Resources managed by this priority: + * <ul> + * <li>Picture processing via {@link MediaCodec.KEY_PICTURE_PROFILE}</li> + * <li>Picture processing via {@link SurfaceControl.Transaction#setPictureProfileHandle} + * </li> + * </ul> + * + * @param sc The SurfaceControl of the layer that should be updated + * @param priority The priority this layer should have with respect to other layers in the + * app. The default priority is zero. + * + * @see MediaQualityManager#getMaxPictureProfiles + * @see MediaCodec.KEY_PICTURE_PROFILE + * @see SurfaceControl.Transaction#setPictureProfileHandle + */ + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) + public @NonNull Transaction setContentPriority(@NonNull SurfaceControl sc, + int priority) { + checkPreconditions(sc); + + nativeSetContentPriority(mNativeObject, sc.mNativeObject, priority); + return this; + } + + /** * Sets the caching hint for the layer. By default, the caching hint is * {@link CACHING_ENABLED}. * diff --git a/core/java/android/view/SurfaceControlActivePicture.java b/core/java/android/view/SurfaceControlActivePicture.java new file mode 100644 index 000000000000..569159d73d2d --- /dev/null +++ b/core/java/android/view/SurfaceControlActivePicture.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 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.view; + +import android.annotation.NonNull; +import android.media.quality.PictureProfileHandle; + +/** + * A record of a visible layer that is using picture processing. + * @hide + */ +public class SurfaceControlActivePicture { + private final int mLayerId; + private final int mOwnerUid; + private final @NonNull PictureProfileHandle mPictureProfileHandle; + + /** + * Create a record of a visible layer that is using picture processing. + * + * @param layerId the layer that is using picture processing + * @param ownerUid the UID of the package that owns the layer + * @param handle the handle for the picture profile that configured the processing + */ + private SurfaceControlActivePicture(int layerId, int ownerUid, PictureProfileHandle handle) { + mLayerId = layerId; + mOwnerUid = ownerUid; + mPictureProfileHandle = handle; + } + + /** The layer that is using picture processing. */ + public int getLayerId() { + return mLayerId; + } + + /** The UID of the package that owns the layer using picture processing. */ + public int getOwnerUid() { + return mOwnerUid; + } + + /** A handle that indicates which picture profile has configured the picture processing. */ + public @NonNull PictureProfileHandle getPictureProfileHandle() { + return mPictureProfileHandle; + } +} diff --git a/core/java/android/view/SurfaceControlActivePictureListener.java b/core/java/android/view/SurfaceControlActivePictureListener.java new file mode 100644 index 000000000000..d7c53748784f --- /dev/null +++ b/core/java/android/view/SurfaceControlActivePictureListener.java @@ -0,0 +1,69 @@ +/* + * Copyright 2024 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.view; + +import android.annotation.RequiresPermission; + +import libcore.util.NativeAllocationRegistry; + +/** + * Allows for the monitoring of visible layers that are using picture processing. + * @hide + */ +public abstract class SurfaceControlActivePictureListener { + private static final NativeAllocationRegistry sRegistry = + NativeAllocationRegistry.createMalloced( + SurfaceControlActivePictureListener.class.getClassLoader(), + nativeGetDestructor()); + + /** + * Callback when there are changes in the visible layers that are using picture processing. + * + * @param activePictures The visible layers that are using picture processing. + */ + public abstract void onActivePicturesChanged(SurfaceControlActivePicture[] activePictures); + + /** + * Start listening to changes in active pictures. + */ + @RequiresPermission(android.Manifest.permission.OBSERVE_PICTURE_PROFILES) + public void startListening() { + synchronized (this) { + long nativePtr = nativeMakeAndStartListening(); + mDestructor = sRegistry.registerNativeAllocation(this, nativePtr); + } + } + + /** + * Stop listening to changes in active pictures. + */ + @RequiresPermission(android.Manifest.permission.OBSERVE_PICTURE_PROFILES) + public void stopListening() { + final Runnable destructor; + synchronized (this) { + destructor = mDestructor; + } + if (destructor != null) { + destructor.run(); + } + } + + private native long nativeMakeAndStartListening(); + private static native long nativeGetDestructor(); + + private Runnable mDestructor; +} diff --git a/core/jni/Android.bp b/core/jni/Android.bp index a21bf9abdd7b..5c03c5cca66d 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -161,6 +161,7 @@ cc_library_shared_for_libandroid_runtime { "android_view_MotionPredictor.cpp", "android_view_PointerIcon.cpp", "android_view_SurfaceControl.cpp", + "android_view_SurfaceControlActivePictureListener.cpp", "android_view_SurfaceControlHdrLayerInfoListener.cpp", "android_view_WindowManagerGlobal.cpp", "android_graphics_BLASTBufferQueue.cpp", diff --git a/core/jni/AndroidRuntime.cpp b/core/jni/AndroidRuntime.cpp index 821861efd59b..00a62977de43 100644 --- a/core/jni/AndroidRuntime.cpp +++ b/core/jni/AndroidRuntime.cpp @@ -128,6 +128,7 @@ extern int register_android_view_InputApplicationHandle(JNIEnv* env); extern int register_android_view_InputWindowHandle(JNIEnv* env); extern int register_android_view_Surface(JNIEnv* env); extern int register_android_view_SurfaceControl(JNIEnv* env); +extern int register_android_view_SurfaceControlActivePictureListener(JNIEnv* env); extern int register_android_view_SurfaceControlHdrLayerInfoListener(JNIEnv* env); extern int register_android_view_SurfaceSession(JNIEnv* env); extern int register_android_view_CompositionSamplingListener(JNIEnv* env); @@ -1563,6 +1564,7 @@ static const RegJNIRec gRegJNI[] = { REG_JNI(register_android_view_DisplayEventReceiver), REG_JNI(register_android_view_Surface), REG_JNI(register_android_view_SurfaceControl), + REG_JNI(register_android_view_SurfaceControlActivePictureListener), REG_JNI(register_android_view_SurfaceControlHdrLayerInfoListener), REG_JNI(register_android_view_SurfaceSession), REG_JNI(register_android_view_InputApplicationHandle), diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 593b982d4cf2..68e642086636 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -55,6 +55,7 @@ #include <ui/FrameStats.h> #include <ui/GraphicTypes.h> #include <ui/HdrCapabilities.h> +#include <ui/PictureProfileHandle.h> #include <ui/Rect.h> #include <ui/Region.h> #include <ui/StaticDisplayInfo.h> @@ -820,6 +821,21 @@ static void nativeSetLuts(JNIEnv* env, jclass clazz, jlong transactionObj, jlong transaction->setLuts(ctrl, base::unique_fd(fd), offsets, dimensions, sizes, samplingKeys); } +static void nativeSetPictureProfileId(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong surfaceControlObj, jlong pictureProfileId) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + SurfaceControl* const surfaceControl = reinterpret_cast<SurfaceControl*>(surfaceControlObj); + PictureProfileHandle handle(pictureProfileId); + transaction->setPictureProfileHandle(surfaceControl, handle); +} + +static void nativeSetContentPriority(JNIEnv* env, jclass clazz, jlong transactionObj, + jlong surfaceControlObj, jint priority) { + auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); + SurfaceControl* const surfaceControl = reinterpret_cast<SurfaceControl*>(surfaceControlObj); + transaction->setContentPriority(surfaceControl, priority); +} + static void nativeSetCachingHint(JNIEnv* env, jclass clazz, jlong transactionObj, jlong nativeObject, jint cachingHint) { auto transaction = reinterpret_cast<SurfaceComposerClient::Transaction*>(transactionObj); @@ -2351,6 +2367,20 @@ static jboolean nativeBootFinished(JNIEnv* env, jclass clazz) { return error == OK ? JNI_TRUE : JNI_FALSE; } +static jint nativeGetMaxPictureProfiles(JNIEnv* env, jclass clazz) { + const auto displayIds = SurfaceComposerClient::SurfaceComposerClient::getPhysicalDisplayIds(); + int largestMaxProfiles = 0; + for (auto displayId : displayIds) { + sp<IBinder> token = SurfaceComposerClient::getPhysicalDisplayToken(displayId); + int32_t maxProfiles = 0; + SurfaceComposerClient::getMaxLayerPictureProfiles(token, &maxProfiles); + if (maxProfiles > largestMaxProfiles) { + largestMaxProfiles = maxProfiles; + } + } + return largestMaxProfiles; +} + jlong nativeCreateTpc(JNIEnv* env, jclass clazz, jobject trustedPresentationCallback) { return reinterpret_cast<jlong>( new TrustedPresentationCallbackWrapper(env, trustedPresentationCallback)); @@ -2672,6 +2702,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { (void*)nativeGetDefaultApplyToken }, {"nativeBootFinished", "()Z", (void*)nativeBootFinished }, + {"nativeGetMaxPictureProfiles", "()I", + (void*)nativeGetMaxPictureProfiles }, {"nativeCreateTpc", "(Landroid/view/SurfaceControl$TrustedPresentationCallback;)J", (void*)nativeCreateTpc}, {"getNativeTrustedPresentationCallbackFinalizer", "()J", (void*)getNativeTrustedPresentationCallbackFinalizer }, @@ -2683,6 +2715,8 @@ static const JNINativeMethod sSurfaceControlMethods[] = { (void*)nativeNotifyShutdown }, {"nativeSetLuts", "(JJ[F[I[I[I[I)V", (void*)nativeSetLuts }, {"nativeEnableDebugLogCallPoints", "(J)V", (void*)nativeEnableDebugLogCallPoints }, + {"nativeSetPictureProfileId", "(JJJ)V", (void*)nativeSetPictureProfileId }, + {"nativeSetContentPriority", "(JJI)V", (void*)nativeSetContentPriority }, // clang-format on }; diff --git a/core/jni/android_view_SurfaceControlActivePictureListener.cpp b/core/jni/android_view_SurfaceControlActivePictureListener.cpp new file mode 100644 index 000000000000..91849c1514cc --- /dev/null +++ b/core/jni/android_view_SurfaceControlActivePictureListener.cpp @@ -0,0 +1,191 @@ +/* + * Copyright 2021 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. + */ + +#define LOG_TAG "SurfaceControlActivePictureListener" + +#include <android/gui/BnActivePictureListener.h> +#include <android_runtime/Log.h> +#include <gui/ISurfaceComposer.h> +#include <gui/SurfaceComposerClient.h> +#include <log/log.h> +#include <nativehelper/JNIHelp.h> +#include <utils/RefBase.h> + +#include "android_util_Binder.h" +#include "core_jni_helpers.h" + +namespace android { + +namespace { + +struct { + jclass clazz; + jmethodID onActivePicturesChanged; +} gListenerClassInfo; + +static struct { + jclass clazz; + jmethodID constructor; +} gActivePictureClassInfo; + +static struct { + jclass clazz; + jmethodID constructor; + jfieldID id; +} gPictureProfileHandleClassInfo; + +struct SurfaceControlActivePictureListener : public gui::BnActivePictureListener { + SurfaceControlActivePictureListener(JNIEnv* env, jobject listener) + : mListener(env->NewGlobalRef(listener)) { + LOG_ALWAYS_FATAL_IF(env->GetJavaVM(&mVm) != JNI_OK, "Failed to GetJavaVm"); + } + + binder::Status onActivePicturesChanged( + const std::vector<gui::ActivePicture>& activePictures) override { + JNIEnv* env = requireEnv(); + + ScopedLocalRef<jobjectArray> activePictureArrayObj(env); + activePictureArrayObj.reset( + env->NewObjectArray(activePictures.size(), gActivePictureClassInfo.clazz, NULL)); + if (env->ExceptionCheck() || !activePictureArrayObj.get()) { + LOGE_EX(env); + LOG_ALWAYS_FATAL("Failed to create an active picture array."); + } + + { + std::vector<ScopedLocalRef<jobject>> pictureProfileHandleObjs; + std::vector<ScopedLocalRef<jobject>> activePictureObjs; + + for (size_t i = 0; i < activePictures.size(); ++i) { + pictureProfileHandleObjs.push_back(ScopedLocalRef<jobject>(env)); + pictureProfileHandleObjs[i].reset( + env->NewObject(gPictureProfileHandleClassInfo.clazz, + gPictureProfileHandleClassInfo.constructor, + activePictures[i].pictureProfileId)); + if (env->ExceptionCheck() || !pictureProfileHandleObjs[i].get()) { + LOGE_EX(env); + LOG_ALWAYS_FATAL("Failed to create a picture profile handle."); + } + activePictureObjs.push_back(ScopedLocalRef<jobject>(env)); + activePictureObjs[i].reset(env->NewObject(gActivePictureClassInfo.clazz, + gActivePictureClassInfo.constructor, + activePictures[i].layerId, + activePictures[i].ownerUid, + pictureProfileHandleObjs[i].get())); + if (env->ExceptionCheck() || !activePictureObjs[i].get()) { + LOGE_EX(env); + LOG_ALWAYS_FATAL("Failed to create an active picture."); + } + env->SetObjectArrayElement(activePictureArrayObj.get(), i, + activePictureObjs[i].get()); + } + + env->CallVoidMethod(mListener, gListenerClassInfo.onActivePicturesChanged, + activePictureArrayObj.get()); + } + + if (env->ExceptionCheck()) { + ALOGE("SurfaceControlActivePictureListener.onActivePicturesChanged failed"); + LOGE_EX(env); + env->ExceptionClear(); + } + return binder::Status::ok(); + } + + status_t startListening() { + // TODO(b/337330263): Make SF multiple-listener capable + return SurfaceComposerClient::setActivePictureListener(this); + } + + status_t stopListening() { + return SurfaceComposerClient::setActivePictureListener(nullptr); + } + +protected: + virtual ~SurfaceControlActivePictureListener() { + JNIEnv* env = requireEnv(); + env->DeleteGlobalRef(mListener); + } + + JNIEnv* requireEnv() { + JNIEnv* env = nullptr; + if (mVm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { + if (mVm->AttachCurrentThreadAsDaemon(&env, nullptr) != JNI_OK) { + LOG_ALWAYS_FATAL("Failed to AttachCurrentThread!"); + } + } + return env; + } + +private: + jobject mListener; + JavaVM* mVm; +}; + +jlong nativeMakeAndStartListening(JNIEnv* env, jobject jthis) { + auto listener = sp<SurfaceControlActivePictureListener>::make(env, jthis); + status_t err = listener->startListening(); + if (err != OK) { + auto errStr = statusToString(err); + jniThrowExceptionFmt(env, "java/lang/IllegalStateException", + "Failed to start listening, err = %d (%s)", err, errStr.c_str()); + return 0; + } + SurfaceControlActivePictureListener* listenerRawPtr = listener.get(); + listenerRawPtr->incStrong(0); + return static_cast<jlong>(reinterpret_cast<intptr_t>(listenerRawPtr)); +} + +static void destroy(SurfaceControlActivePictureListener* listener) { + listener->stopListening(); + listener->decStrong(0); +} + +static jlong nativeGetDestructor(JNIEnv* env, jobject clazz) { + return static_cast<jlong>(reinterpret_cast<intptr_t>(&destroy)); +} + +const JNINativeMethod gMethods[] = { + /* name, signature, funcPtr */ + {"nativeGetDestructor", "()J", (void*)nativeGetDestructor}, + {"nativeMakeAndStartListening", "()J", (void*)nativeMakeAndStartListening}}; +} // namespace + +int register_android_view_SurfaceControlActivePictureListener(JNIEnv* env) { + int res = jniRegisterNativeMethods(env, "android/view/SurfaceControlActivePictureListener", + gMethods, NELEM(gMethods)); + LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods."); + + jclass listenerClazz = env->FindClass("android/view/SurfaceControlActivePictureListener"); + gListenerClassInfo.clazz = MakeGlobalRefOrDie(env, listenerClazz); + gListenerClassInfo.onActivePicturesChanged = + env->GetMethodID(listenerClazz, "onActivePicturesChanged", + "([Landroid/view/SurfaceControlActivePicture;)V"); + + gActivePictureClassInfo.clazz = static_cast<jclass>( + env->NewGlobalRef(env->FindClass("android/view/SurfaceControlActivePicture"))); + gActivePictureClassInfo.constructor = + env->GetMethodID(gActivePictureClassInfo.clazz, "<init>", + "(IILandroid/media/quality/PictureProfileHandle;)V"); + + gPictureProfileHandleClassInfo.clazz = static_cast<jclass>( + env->NewGlobalRef(env->FindClass("android/media/quality/PictureProfileHandle"))); + gPictureProfileHandleClassInfo.constructor = + env->GetMethodID(gPictureProfileHandleClassInfo.clazz, "<init>", "(J)V"); + return 0; +} + +} // namespace android diff --git a/media/java/android/media/quality/PictureProfileHandle.java b/media/java/android/media/quality/PictureProfileHandle.java index 2b1cda4eb742..714fd36d664a 100644 --- a/media/java/android/media/quality/PictureProfileHandle.java +++ b/media/java/android/media/quality/PictureProfileHandle.java @@ -17,46 +17,58 @@ package android.media.quality; import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; - -// TODO(b/337330263): Expose as public API after API review /** - * A type-safe handle to a picture profile, which represents a collection of parameters used to - * configure picture processing hardware to enhance the quality of graphic buffers. + * A type-safe handle to a picture profile used to apply picture processing to a SurfaceControl. + * + * A picture profile represents a collection of parameters used to configure picture processing + * to enhance the quality of graphic buffers. + * * @hide */ -@FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) +@SystemApi +@FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) public final class PictureProfileHandle implements Parcelable { + public static final @NonNull PictureProfileHandle NONE = new PictureProfileHandle(0); + private final long mId; - @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) + /** @hide */ public PictureProfileHandle(long id) { mId = id; } - @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) + /** @hide */ + @SystemApi + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) public long getId() { return mId; } - @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) + /** @hide */ + @SystemApi @Override + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeLong(mId); } - @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) + /** @hide */ + @SystemApi @Override + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) public int describeContents() { return 0; } - @FlaggedApi(android.media.tv.flags.Flags.FLAG_MEDIA_QUALITY_FW) - @NonNull - public static final Creator<PictureProfileHandle> CREATOR = + /** @hide */ + @SystemApi + @FlaggedApi(android.media.tv.flags.Flags.FLAG_APPLY_PICTURE_PROFILES) + public static final @NonNull Creator<PictureProfileHandle> CREATOR = new Creator<PictureProfileHandle>() { @Override public PictureProfileHandle createFromParcel(Parcel in) { diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml index d8eb9ff37e78..da510fcd4d63 100644 --- a/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml +++ b/tests/CtsSurfaceControlTestsStaging/AndroidManifest.xml @@ -18,12 +18,20 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="android.view.surfacecontroltests"> + <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER"/> + <uses-permission android:name="android.permission.OBSERVE_PICTURE_PROFILES"/> + <application android:debuggable="true" android:testOnly="true"> <uses-library android:name="android.test.runner"/> <activity android:name=".GraphicsActivity" android:exported="false"> </activity> + <activity android:name=".SurfaceControlPictureProfileTestActivity" + android:exported="true" + android:turnScreenOn="true" + android:showWhenLocked="true" + android:theme="@android:style/Theme.NoTitleBar.Fullscreen" /> </application> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" diff --git a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml index 5c0163fcfa7e..025bf378d82f 100644 --- a/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml +++ b/tests/CtsSurfaceControlTestsStaging/AndroidTest.xml @@ -21,6 +21,9 @@ <option name="install-arg" value="-t" /> <option name="test-file-name" value="CtsSurfaceControlTestsStaging.apk" /> </target_preparer> + <target_preparer class="com.android.tradefed.targetprep.FeatureFlagTargetPreparer" > + <option name="flag-value" value="media_tv/android.media.tv.flags.apply_picture_profiles=true" /> + </target_preparer> <test class="com.android.tradefed.testtype.AndroidJUnitTest" > <option name="package" value="android.view.surfacecontroltests" /> <option name="hidden-api-checks" value="false" /> diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java new file mode 100644 index 000000000000..135f7102b8a9 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTest.java @@ -0,0 +1,260 @@ +/* + * Copyright 2024 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.view.surfacecontroltests; + +import static android.Manifest.permission.OBSERVE_PICTURE_PROFILES; + +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assume.assumeTrue; + +import static java.util.Arrays.stream; + +import android.hardware.HardwareBuffer; +import android.media.quality.PictureProfileHandle; +import android.os.Process; +import android.view.SurfaceControl; +import android.view.SurfaceControlActivePicture; +import android.view.SurfaceControlActivePictureListener; +import android.view.SurfaceView; + +import androidx.test.filters.SmallTest; +import androidx.test.rule.ActivityTestRule; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.stream.LongStream; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class SurfaceControlPictureProfileTest { + private static final String TAG = SurfaceControlPictureProfileTest.class.getSimpleName(); + + private SurfaceControl[] mSurfaceControls; + private SurfaceControl mSurfaceControl; + + @Rule + public ActivityTestRule<SurfaceControlPictureProfileTestActivity> mActivityRule = + new ActivityTestRule<>(SurfaceControlPictureProfileTestActivity.class); + + @Before + public void setup() { + SurfaceView[] surfaceViews = mActivityRule.getActivity().getSurfaceViews(); + mSurfaceControls = new SurfaceControl[surfaceViews.length]; + // Create a child surface control so we can set a buffer, priority and profile handle all + // on one single surface control + for (int i = 0; i < mSurfaceControls.length; ++i) { + mSurfaceControls[i] = new SurfaceControl.Builder().setName("test").setHidden(false) + .setParent(surfaceViews[i].getSurfaceControl()).build(); + } + mSurfaceControl = mSurfaceControls[0]; + } + + @Test + public void whenPictureProfileApplied_noExecptionsThrown() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction() + .setBuffer(mSurfaceControl, buffer) + .setPictureProfileHandle(mSurfaceControl, handle) + .apply(); + } + + @Test + public void whenStartsListening_callsListener() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + } + + @Test + public void whenPictureProfileApplied_callsListenerWithUidAndProfileId() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + assumeTrue("Skipping test because no picture profile support", + SurfaceControl.getMaxPictureProfiles() > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction() + .setBuffer(mSurfaceControl, buffer) + .setPictureProfileHandle(mSurfaceControl, handle) + .apply(); + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactly(handle.getId()); + assertThat(stream(pictures).map(picture -> picture.getOwnerUid())) + .containsExactly(Process.myUid()); + } + + @Test + public void whenPriorityChanges_callsListenerOnlyForLowerPriorityLayers() { + assumeTrue("Skipping test because feature flag is disabled", + com.android.graphics.libgui.flags.Flags.applyPictureProfiles()); + // TODO(b/337330263): Call MediaQualityManager.getMaxPictureProfiles instead + int maxPictureProfiles = SurfaceControl.getMaxPictureProfiles(); + assumeTrue("Skipping test because no picture profile support", maxPictureProfiles > 0); + + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue = new LinkedBlockingQueue<>(); + SurfaceControlActivePicture[] pictures; + SurfaceControlActivePictureListener listener = new SurfaceControlActivePictureListener() { + @Override + public void onActivePicturesChanged(SurfaceControlActivePicture[] pictures) { + picturesQueue.add(pictures); + } + }; + // TODO(b/337330263): Call MediaQualityManager.addActivePictureListener instead + adoptShellPermissionIdentity(OBSERVE_PICTURE_PROFILES); + listener.startListening(); + { + HardwareBuffer buffer = getSolidBuffer(100, 100); + new SurfaceControl.Transaction().setBuffer(mSurfaceControl, buffer).apply(); + } + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(pictures).isEmpty(); + + SurfaceControl.Transaction transaction = new SurfaceControl.Transaction(); + // Use one more picture profile than allowed + for (int i = 0; i <= maxPictureProfiles; ++i) { + // Increase the number of surface views as necessary to support device configuration. + assertThat(i).isLessThan(mSurfaceControls.length); + + // TODO(b/337330263): Load the handle from MediaQualityManager instead + PictureProfileHandle handle = new PictureProfileHandle(i + 1); + HardwareBuffer buffer = getSolidBuffer(100, 100); + transaction + .setBuffer(mSurfaceControls[i], buffer) + .setPictureProfileHandle(mSurfaceControls[i], handle) + .setContentPriority(mSurfaceControls[i], 0); + } + // Make the first layer low priority (high value) + transaction.setContentPriority(mSurfaceControls[0], 2); + // Make the last layer higher priority (lower value) + transaction.setContentPriority(mSurfaceControls[maxPictureProfiles], 1); + transaction.apply(); + + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + assertThat(stream(pictures).map(picture -> picture.getLayerId())) + .containsNoDuplicates(); + // Expect all but the first layer to be listed as an active picture + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactlyElementsIn(toIterableRange(2, maxPictureProfiles + 1)); + + // Change priority and ensure that the first layer gets access + new SurfaceControl.Transaction().setContentPriority(mSurfaceControls[0], 0).apply(); + pictures = pollMs(picturesQueue, 200); + assertThat(pictures).isNotNull(); + // Expect all but the last layer to be listed as an active picture + assertThat(stream(pictures).map(picture -> picture.getPictureProfileHandle().getId())) + .containsExactlyElementsIn(toIterableRange(1, maxPictureProfiles)); + } + + private static SurfaceControlActivePicture[] pollMs( + BlockingQueue<SurfaceControlActivePicture[]> picturesQueue, int waitMs) { + SurfaceControlActivePicture[] pictures = null; + long nowMs = System.currentTimeMillis(); + long endTimeMs = nowMs + waitMs; + while (nowMs < endTimeMs && pictures == null) { + try { + pictures = picturesQueue.poll(endTimeMs - nowMs, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + // continue polling until timeout when interrupted + } + nowMs = System.currentTimeMillis(); + } + return pictures; + } + + Iterable<Long> toIterableRange(int start, int stop) { + return () -> LongStream.rangeClosed(start, stop).iterator(); + } + + private void adoptShellPermissionIdentity(String permission) { + getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(permission); + } + + private HardwareBuffer getSolidBuffer(int width, int height) { + // We can assume that RGBA_8888 format is supported for every platform. + return HardwareBuffer.create( + width, height, HardwareBuffer.RGBA_8888, 1, HardwareBuffer.USAGE_CPU_WRITE_OFTEN); + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java new file mode 100644 index 000000000000..42fcb261fa9d --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/SurfaceControlPictureProfileTestActivity.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 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.view.surfacecontroltests; + +import android.app.Activity; +import android.os.Bundle; +import android.view.SurfaceView; + +public class SurfaceControlPictureProfileTestActivity extends Activity { + private SurfaceView[] mSurfaceViews; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.picture_profile_test_layout); + mSurfaceViews = new SurfaceView[3]; + mSurfaceViews[0] = (SurfaceView) findViewById(R.id.surfaceview1); + mSurfaceViews[1] = (SurfaceView) findViewById(R.id.surfaceview2); + mSurfaceViews[2] = (SurfaceView) findViewById(R.id.surfaceview3); + } + + public SurfaceView getSurfaceView() { + return mSurfaceViews[0]; + } + + public SurfaceView[] getSurfaceViews() { + return mSurfaceViews; + } +} diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml new file mode 100644 index 000000000000..9aa25785d9f2 --- /dev/null +++ b/tests/CtsSurfaceControlTestsStaging/src/main/res/layout/picture_profile_test_layout.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + * Copyright (C) 2024 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. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + <SurfaceView android:id="@+id/surfaceview1" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> + <SurfaceView android:id="@+id/surfaceview2" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> + <SurfaceView android:id="@+id/surfaceview3" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"/> +</LinearLayout> |