diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt index e95f56aeab356..abbb7bf4258d9 100644 --- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt +++ b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderTest.kt @@ -32,6 +32,7 @@ import android.media.MediaCodec import android.media.MediaMetadataRetriever import android.media.MediaRecorder import android.net.Uri +import android.os.Build import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.util.Size @@ -43,14 +44,13 @@ import androidx.camera.core.CameraXConfig import androidx.camera.core.DynamicRange import androidx.camera.core.Preview import androidx.camera.core.SurfaceRequest -import androidx.camera.core.impl.AdapterCameraInfo +import androidx.camera.core.impl.CameraInfoInternal import androidx.camera.core.impl.ImageFormatConstants import androidx.camera.core.impl.Observable.Observer import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.awaitInstance -import androidx.camera.testing.fakes.FakeCameraInfoInternal import androidx.camera.testing.impl.AudioUtil import androidx.camera.testing.impl.CameraPipeConfigTestRule import androidx.camera.testing.impl.CameraUtil @@ -60,7 +60,6 @@ import androidx.camera.testing.impl.IgnoreVideoRecordingProblematicDeviceRule import androidx.camera.testing.impl.LabTestRule import androidx.camera.testing.impl.SurfaceTextureProvider import androidx.camera.testing.impl.asFlow -import androidx.camera.testing.impl.fakes.FakeCameraConfig import androidx.camera.testing.impl.fakes.FakeLifecycleOwner import androidx.camera.testing.impl.fakes.FakeSessionProcessor import androidx.camera.testing.impl.fakes.NoOpMuxer @@ -193,6 +192,8 @@ class RecorderTest(private val implName: String, private val cameraConfig: Camer private lateinit var cameraProvider: ProcessCameraProvider private lateinit var camera: Camera private lateinit var cameraSelector: CameraSelector + private lateinit var cameraInfoInternal: CameraInfoInternal + private lateinit var videoCapabilities: VideoCapabilities private lateinit var preview: Preview private lateinit var surfaceTexturePreview: Preview @@ -220,7 +221,8 @@ class RecorderTest(private val implName: String, private val cameraConfig: Camer // Using Preview so that the surface provider could be set to control when to issue the // surface request. val cameraInfo = camera.cameraInfo - val videoCapabilities = Recorder.getVideoCapabilities(cameraInfo) + cameraInfoInternal = cameraInfo as CameraInfoInternal + videoCapabilities = Recorder.getVideoCapabilities(cameraInfo) val candidates = mutableSetOf().apply { if (testName.methodName == "setFileSizeLimit") { @@ -1258,138 +1260,41 @@ class RecorderTest(private val implName: String, private val cameraConfig: Camer } @Test - fun getVideoCapabilities_returnsCachedInstanceForSameCameraInfo() { - // Arrange - val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) + fun getVideoCapabilities_supportStandardDynamicRange() { + assumeFalse(isDeviceWithCamcorderProfileResolutionMismatch()) - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo) - - // Assert - assertThat(capabilities1).isSameInstanceAs(capabilities2) - } - - @Test - fun getVideoCapabilities_returnsDifferentInstanceForDifferentCameraInfos() { - // Arrange - val cameraInfos = cameraProvider.availableCameraInfos - assumeTrue("The device must have at least 2 cameras.", cameraInfos.size >= 2) - val cameraInfo1 = cameraInfos[0] - val cameraInfo2 = cameraInfos[1] - - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo1) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo2) - - // Assert - assertThat(capabilities1).isNotSameInstanceAs(capabilities2) - } - - @Test - fun getVideoCapabilities_returnsCachedInstanceForDifferentCameraInfoWithSameIdAndConfig() { - // Arrange - val cameraConfig = FakeCameraConfig() - val cameraInfo1 = AdapterCameraInfo(FakeCameraInfoInternal("0"), cameraConfig) - val cameraInfo2 = AdapterCameraInfo(FakeCameraInfoInternal("0"), cameraConfig) - - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo1) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo2) - - // Assert - assertThat(capabilities1).isSameInstanceAs(capabilities2) - } - - @Test - fun getVideoCapabilities_returnsCachedInstanceForCameraInfoOfNewBinding() = runBlocking { - // Arrange & act - val capabilities1 = Recorder.getVideoCapabilities(camera.cameraInfo) - val capabilities2 = - Recorder.getVideoCapabilities( - withContext(Dispatchers.Main) { - cameraProvider - .bindToLifecycle( - FakeLifecycleOwner().also { it.startAndResume() }, - cameraSelector, - Preview.Builder().build(), - ) - .cameraInfo - } - ) - - // Assert - assertThat(capabilities1).isSameInstanceAs(capabilities2) + assertThat(videoCapabilities.supportedDynamicRanges).contains(DynamicRange.SDR) } @Test - fun getVideoCapabilities_doesNotCacheForExternalCamera() { - // Arrange - val cameraInfo = - AdapterCameraInfo( - FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_EXTERNAL), - FakeCameraConfig(), - ) - - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo) + fun getVideoCapabilities_supportedQualitiesOfSdrIsNotEmpty() { + assumeFalse(isDeviceWithCamcorderProfileResolutionMismatch()) - // Assert - assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + assertThat(videoCapabilities.getSupportedQualities(DynamicRange.SDR)).isNotEmpty() } - @Test - fun getVideoCapabilities_doesNotCacheForUnknownLensFacingCamera() { - // Arrange - val cameraInfo = - AdapterCameraInfo( - FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_UNKNOWN), - FakeCameraConfig(), - ) + /** + * Checks if the device has a known mismatch between CamcorderProfile resolutions and the + * camera's supported output sizes (b/231903433). + * + * See go/camerax-camcorder-profile-no-matching-resolutions + */ + private fun isDeviceWithCamcorderProfileResolutionMismatch(): Boolean { + val isNokia2Point1 = + "nokia".equals(Build.BRAND, true) && "nokia 2.1".equals(Build.MODEL, true) + val isMotoE5Play = + "motorola".equals(Build.BRAND, true) && "moto e5 play".equals(Build.MODEL, true) - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo) - - // Assert - assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + return isNokia2Point1 || isMotoE5Play } @Test - fun getVideoCapabilities_returnsDifferentInstancesForDifferentCameraConfigs() { - // Arrange - val cameraInfo1 = AdapterCameraInfo(FakeCameraInfoInternal("0"), FakeCameraConfig()) - val cameraInfo2 = AdapterCameraInfo(FakeCameraInfoInternal("0"), FakeCameraConfig()) - - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo1) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo2) - - // Assert - assertThat(capabilities1).isNotSameInstanceAs(capabilities2) - } - - @Test - fun getVideoCapabilities_returnsDifferentInstancesForExtensionCameraSelector() { - // Arrange - val cameraInfo1 = cameraProvider.getCameraInfo(cameraSelector) - - val sessionProcessor = FakeSessionProcessor() - val cameraSelector2 = - ExtensionsUtil.getCameraSelectorWithSessionProcessor( - cameraProvider, - cameraSelector, - sessionProcessor, - ) - val cameraInfo2 = cameraProvider.getCameraInfo(cameraSelector2) + fun getHighSpeedVideoCapabilities_whenCameraDoesNotSupportHighSpeed_returnNull() { + assumeFalse(cameraInfoInternal.isHighSpeedSupported) - // Act - val capabilities1 = Recorder.getVideoCapabilities(cameraInfo1) - val capabilities2 = Recorder.getVideoCapabilities(cameraInfo2) + val videoCapabilities = Recorder.getHighSpeedVideoCapabilities(camera.cameraInfo) - // Assert - assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + assertThat(videoCapabilities).isNull() } private fun testRecorderIsConfiguredBasedOnTargetVideoEncodingBitrate(targetBitrate: Int) { diff --git a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt b/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt deleted file mode 100644 index 1110fac96eb98..0000000000000 --- a/camera/camera-video/src/androidTest/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2023 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 androidx.camera.video - -import android.content.Context -import android.os.Build -import androidx.camera.camera2.Camera2Config -import androidx.camera.camera2.pipe.integration.CameraPipeConfig -import androidx.camera.core.CameraXConfig -import androidx.camera.core.DynamicRange.SDR -import androidx.camera.core.impl.CameraInfoInternal -import androidx.camera.testing.impl.AndroidUtil.isEmulator -import androidx.camera.testing.impl.CameraPipeConfigTestRule -import androidx.camera.testing.impl.CameraUtil -import androidx.camera.testing.impl.CameraXUtil -import androidx.camera.video.Quality.QUALITY_SOURCE_REGULAR -import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE -import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES -import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl -import androidx.test.core.app.ApplicationProvider -import androidx.test.filters.SmallTest -import com.google.common.truth.Truth.assertThat -import java.util.concurrent.TimeUnit -import org.junit.After -import org.junit.Assume.assumeFalse -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -@SmallTest -class RecorderVideoCapabilitiesTest( - private val implName: String, - private val cameraConfig: CameraXConfig, -) { - - @get:Rule - val cameraPipeConfigTestRule = - CameraPipeConfigTestRule(active = implName == CameraPipeConfig::class.simpleName) - - @get:Rule - val cameraRule = - CameraUtil.grantCameraPermissionAndPreTestAndPostTest( - CameraUtil.PreTestCameraIdList(cameraConfig) - ) - - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{0}") - fun data() = - listOf( - arrayOf(Camera2Config::class.simpleName, Camera2Config.defaultConfig()), - arrayOf(CameraPipeConfig::class.simpleName, CameraPipeConfig.defaultConfig()), - ) - } - - private val context: Context = ApplicationProvider.getApplicationContext() - - private lateinit var cameraInfo: CameraInfoInternal - private lateinit var videoCapabilities: RecorderVideoCapabilities - - @Before - fun setUp() { - // Skip for b/264902324 - assumeFalse( - "Emulator API 30 crashes running this test.", - Build.VERSION.SDK_INT == 30 && isEmulator(), - ) - - val cameraSelector = CameraUtil.assumeFirstAvailableCameraSelector() - - CameraXUtil.initialize(context, cameraConfig).get() - - cameraInfo = - CameraUtil.createCameraUseCaseAdapter(context, cameraSelector).cameraInfo - as CameraInfoInternal - videoCapabilities = - RecorderVideoCapabilities( - VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, - cameraInfo, - QUALITY_SOURCE_REGULAR, - VideoEncoderInfoImpl.FINDER, - ) - } - - @After - fun tearDown() { - CameraXUtil.shutdown().get(10, TimeUnit.SECONDS) - } - - @Test - fun supportStandardDynamicRange() { - assumeFalse(isSpecificSkippedDevice()) - assertThat(videoCapabilities.supportedDynamicRanges).contains(SDR) - } - - @Test - fun supportedQualitiesOfSdrIsNotEmpty() { - assumeFalse(isSpecificSkippedDevice()) - assertThat(videoCapabilities.getSupportedQualities(SDR)).isNotEmpty() - } - - @Test - fun whenCameraDoesNotSupportHighSpeed_highSpeedVideoCapabilitiesIsEmpty() { - assumeFalse(cameraInfo.isHighSpeedSupported) - - for (sourceType in - listOf( - VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, - VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES, - )) { - val capabilities = - RecorderVideoCapabilities( - sourceType, - cameraInfo, - Recorder.VIDEO_RECORDING_TYPE_HIGH_SPEED, - VideoEncoderInfoImpl.FINDER, - ) - assertThat(capabilities.supportedDynamicRanges).isEmpty() - } - } - - private fun isSpecificSkippedDevice(): Boolean { - // skip for b/231903433 - val isNokia2Point1 = - "nokia".equals(Build.BRAND, true) && "nokia 2.1".equals(Build.MODEL, true) - val isMotoE5Play = - "motorola".equals(Build.BRAND, true) && "moto e5 play".equals(Build.MODEL, true) - - return isNokia2Point1 || isMotoE5Play - } -} diff --git a/camera/camera-video/src/main/java/androidx/camera/video/EncoderProfilesProviderResolver.kt b/camera/camera-video/src/main/java/androidx/camera/video/EncoderProfilesProviderResolver.kt new file mode 100644 index 0000000000000..f3038635fc302 --- /dev/null +++ b/camera/camera-video/src/main/java/androidx/camera/video/EncoderProfilesProviderResolver.kt @@ -0,0 +1,124 @@ +/* + * 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 androidx.camera.video + +import androidx.camera.core.DynamicRange +import androidx.camera.core.Logger +import androidx.camera.core.impl.CameraInfoInternal +import androidx.camera.core.impl.EncoderProfilesProvider +import androidx.camera.core.impl.ImageFormatConstants +import androidx.camera.video.internal.BackupHdrProfileEncoderProfilesProvider +import androidx.camera.video.internal.QualityExploredEncoderProfilesProvider +import androidx.camera.video.internal.compat.quirk.DeviceQuirks +import androidx.camera.video.internal.encoder.VideoEncoderInfo +import androidx.camera.video.internal.workaround.DefaultEncoderProfilesProvider +import androidx.camera.video.internal.workaround.QualityAddedEncoderProfilesProvider +import androidx.camera.video.internal.workaround.QualityResolutionModifiedEncoderProfilesProvider +import androidx.camera.video.internal.workaround.QualityValidatedEncoderProfilesProvider +import java.util.Collections + +/** Resolver for providing [EncoderProfilesProvider] used by [Recorder]. */ +internal object EncoderProfilesProviderResolver { + + private const val TAG = "EncoderProfilesResolver" + + /** Resolves the [EncoderProfilesProvider] based on the camera info and source. */ + fun resolve( + cameraInfo: CameraInfoInternal, + @Recorder.VideoCapabilitiesSource videoCapabilitiesSource: Int, + @Quality.QualitySource qualitySource: Int, + videoEncoderInfoFinder: VideoEncoderInfo.Finder, + ): EncoderProfilesProvider { + require( + videoCapabilitiesSource == Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE || + videoCapabilitiesSource == Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES + ) { + "Not a supported video capabilities source: $videoCapabilitiesSource" + } + + var provider = cameraInfo.encoderProfilesProvider + + if (qualitySource == Quality.QUALITY_SOURCE_HIGH_SPEED) { + if (!cameraInfo.isHighSpeedSupported) { + return EncoderProfilesProvider.EMPTY + } + + // TODO(b/399585664): explore high speed quality when video source is + // VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES + return provider + } + + if (!CapabilitiesByQuality.containsSupportedQuality(provider, qualitySource)) { + Logger.w(TAG, "Camera EncoderProfilesProvider doesn't contain any supported Quality.") + // Limit maximum supported video resolution to 1080p(FHD). + // While 2160p(UHD) may be reported as supported by the Camera and MediaCodec APIs, + // testing on lab devices has shown that recording at this resolution is not always + // reliable. This aligns with the Android 5.1 CDD, which recommends 1080p as the + // supported resolution. + // See: https://source.android.com/static/docs/compatibility/5.1/android-5.1-cdd.pdf, + // 5.2. Video Encoding. + val targetQualities = listOf(Quality.FHD, Quality.HD, Quality.SD) + provider = + DefaultEncoderProfilesProvider(cameraInfo, targetQualities, videoEncoderInfoFinder) + } + + val deviceQuirks = DeviceQuirks.getAll() + + // Decorate with extra supported qualities + provider = + QualityAddedEncoderProfilesProvider( + provider, + deviceQuirks, + cameraInfo, + videoEncoderInfoFinder, + ) + + if (videoCapabilitiesSource == Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES) { + provider = + QualityExploredEncoderProfilesProvider( + provider, + Quality.getSortedQualities(), + Collections.singleton(DynamicRange.SDR), + cameraInfo.getSupportedResolutions( + ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE + ), + videoEncoderInfoFinder, + ) + } + + // Modify matching resolutions based on camera support + provider = QualityResolutionModifiedEncoderProfilesProvider(provider, deviceQuirks) + + // Add backup HDR profiles (HLG10) + if (cameraInfo.isHlg10Supported) { + provider = BackupHdrProfileEncoderProfilesProvider(provider, videoEncoderInfoFinder) + } + + // Filter for validated qualities + provider = QualityValidatedEncoderProfilesProvider(provider, cameraInfo, deviceQuirks) + + return provider + } + + /** Extension property to check HLG10 support from supported dynamic ranges. */ + private val CameraInfoInternal.isHlg10Supported: Boolean + get() = + supportedDynamicRanges.any { + it.encoding == DynamicRange.ENCODING_HLG && + it.bitDepth == DynamicRange.BIT_DEPTH_10_BIT + } +} diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java index 7b91826cf0eb9..39fe3c27db514 100644 --- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java +++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java @@ -55,7 +55,6 @@ import android.os.Build; import android.os.ParcelFileDescriptor; import android.provider.MediaStore; -import android.util.LruCache; import android.util.Range; import android.util.Rational; import android.util.Size; @@ -70,13 +69,9 @@ import androidx.annotation.VisibleForTesting; import androidx.camera.core.AspectRatio; import androidx.camera.core.CameraInfo; -import androidx.camera.core.CameraSelector; import androidx.camera.core.DynamicRange; import androidx.camera.core.Logger; import androidx.camera.core.SurfaceRequest; -import androidx.camera.core.impl.AdapterCameraInfo; -import androidx.camera.core.impl.CameraConfig; -import androidx.camera.core.impl.CameraInfoInternal; import androidx.camera.core.impl.MutableStateObservable; import androidx.camera.core.impl.Observable; import androidx.camera.core.impl.StateObservable; @@ -109,7 +104,6 @@ import androidx.camera.video.internal.encoder.OutputConfig; import androidx.camera.video.internal.encoder.VideoEncoderConfig; import androidx.camera.video.internal.encoder.VideoEncoderInfo; -import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl; import androidx.camera.video.internal.muxer.MediaMuxerImpl; import androidx.camera.video.internal.muxer.Muxer; import androidx.camera.video.internal.muxer.MuxerException; @@ -381,11 +375,6 @@ enum AudioState { "Insufficient storage space. The available storage (%d bytes) is below the required " + "threshold of %d bytes."; - @GuardedBy("sVideoCapabilitiesCache") - // A size of 16 is likely more than enough for all camera/config combinations on a device. - private static final LruCache - sVideoCapabilitiesCache = new LruCache<>(16); - @VisibleForTesting static int sRetrySetupVideoMaxCount = RETRY_SETUP_VIDEO_MAX_COUNT; @VisibleForTesting @@ -3172,66 +3161,8 @@ private static int supportedMuxerFormatOrDefaultFrom( @VideoRecordingType int videoRecordingType, @NonNull CameraInfo cameraInfo, @VideoCapabilitiesSource int videoCapabilitiesSource) { - if (shouldSkipCapabilitiesCache(cameraInfo)) { - return new RecorderVideoCapabilities(videoCapabilitiesSource, - (CameraInfoInternal) cameraInfo, videoRecordingType, - VideoEncoderInfoImpl.FINDER); - } - - AdapterCameraInfo adapterCameraInfo = (AdapterCameraInfo) cameraInfo; - - String cameraId = adapterCameraInfo.getCameraId(); - CameraConfig cameraConfig = adapterCameraInfo.getCameraConfig(); - VideoCapabilitiesCacheKey key = VideoCapabilitiesCacheKey.create(cameraId, cameraConfig, - videoRecordingType, videoCapabilitiesSource); - - synchronized (sVideoCapabilitiesCache) { - VideoCapabilities capabilities = sVideoCapabilitiesCache.get(key); - - if (capabilities == null) { - capabilities = new RecorderVideoCapabilities(videoCapabilitiesSource, - (CameraInfoInternal) cameraInfo, videoRecordingType, - VideoEncoderInfoImpl.FINDER); - sVideoCapabilitiesCache.put(key, capabilities); - } - return capabilities; - } - } - - /** - * Checks whether the video capabilities for a given camera should be cached. - * - *

Caching is skipped for external cameras or cameras with an unknown lens facing, as their - * properties may not be stable across device reboots or during camera hot-plugging. - */ - private static boolean shouldSkipCapabilitiesCache(@NonNull CameraInfo cameraInfo) { - if (cameraInfo instanceof AdapterCameraInfo) { - AdapterCameraInfo adapterCameraInfo = (AdapterCameraInfo) cameraInfo; - return adapterCameraInfo.isExternalCamera() - || adapterCameraInfo.getLensFacing() == CameraSelector.LENS_FACING_UNKNOWN; - } - // If we can't determine the camera properties (e.g., not an AdapterCameraInfo), - // it's safer to skip caching. - return true; - } - - @SuppressWarnings("unused") // Use as a key class, which methods are not called directly. - @AutoValue - abstract static class VideoCapabilitiesCacheKey { - static VideoCapabilitiesCacheKey create(@NonNull String cameraId, - @NonNull CameraConfig cameraConfig, @VideoRecordingType int videoRecordingType, - @VideoCapabilitiesSource int videoCapabilitiesSource) { - return new AutoValue_Recorder_VideoCapabilitiesCacheKey(cameraId, cameraConfig, - videoRecordingType, videoCapabilitiesSource); - } - - abstract @NonNull String getCameraId(); - - abstract @NonNull CameraConfig getCameraConfig(); - - abstract @VideoRecordingType int getVideoRecordingType(); - - abstract @VideoCapabilitiesSource int getVideoCapabilitiesSource(); + return RecorderVideoCapabilitiesFactory.getCapabilities(cameraInfo, videoRecordingType, + videoCapabilitiesSource); } @AutoValue diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java index ad38d0532fa4a..999e80d4da83b 100644 --- a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java +++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilities.java @@ -16,52 +16,24 @@ package androidx.camera.video; -import static androidx.camera.core.DynamicRange.ENCODING_HLG; -import static androidx.camera.core.DynamicRange.SDR; -import static androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE; -import static androidx.camera.video.CapabilitiesByQuality.containsSupportedQuality; -import static androidx.camera.video.Quality.FHD; -import static androidx.camera.video.Quality.HD; -import static androidx.camera.video.Quality.QUALITY_SOURCE_HIGH_SPEED; -import static androidx.camera.video.Quality.QUALITY_SOURCE_REGULAR; -import static androidx.camera.video.Quality.SD; -import static androidx.camera.video.Quality.getSortedQualities; -import static androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE; -import static androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES; -import static androidx.camera.video.Recorder.VIDEO_RECORDING_TYPE_HIGH_SPEED; -import static androidx.core.util.Preconditions.checkArgument; - -import static java.util.Collections.singleton; - import android.util.Size; import androidx.annotation.RestrictTo; import androidx.camera.core.CameraInfo; import androidx.camera.core.DynamicRange; -import androidx.camera.core.Logger; import androidx.camera.core.impl.CameraInfoInternal; import androidx.camera.core.impl.DynamicRanges; import androidx.camera.core.impl.EncoderProfilesProvider; import androidx.camera.core.impl.EncoderProfilesProxy; import androidx.camera.core.impl.EncoderProfilesProxy.VideoProfileProxy; -import androidx.camera.core.impl.Quirks; import androidx.camera.video.Quality.QualitySource; -import androidx.camera.video.internal.BackupHdrProfileEncoderProfilesProvider; import androidx.camera.video.internal.DynamicRangeMatchedEncoderProfilesProvider; -import androidx.camera.video.internal.QualityExploredEncoderProfilesProvider; import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy; -import androidx.camera.video.internal.compat.quirk.DeviceQuirks; -import androidx.camera.video.internal.encoder.VideoEncoderInfo; -import androidx.camera.video.internal.workaround.DefaultEncoderProfilesProvider; -import androidx.camera.video.internal.workaround.QualityAddedEncoderProfilesProvider; -import androidx.camera.video.internal.workaround.QualityResolutionModifiedEncoderProfilesProvider; -import androidx.camera.video.internal.workaround.QualityValidatedEncoderProfilesProvider; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -71,16 +43,12 @@ * RecorderVideoCapabilities is used to query video recording capabilities related to Recorder. * *

The {@link EncoderProfilesProxy} queried from RecorderVideoCapabilities will contain - * {@link VideoProfileProxy}s matches with the target {@link DynamicRange}. When HDR is - * supported, RecorderVideoCapabilities will try best to provide additional backup HDR - * {@link VideoProfileProxy}s in case the information is lacked in the device. + * {@link VideoProfileProxy}s matches with the target {@link DynamicRange}. * * @see Recorder#getVideoCapabilities(CameraInfo) */ @RestrictTo(RestrictTo.Scope.LIBRARY) public class RecorderVideoCapabilities implements VideoCapabilities { - private static final String TAG = "RecorderVideoCapabilities"; - private final EncoderProfilesProvider mProfilesProvider; private final boolean mIsStabilizationSupported; private final @QualitySource int mQualitySource; @@ -98,28 +66,19 @@ public class RecorderVideoCapabilities implements VideoCapabilities { /** * Creates a RecorderVideoCapabilities. * - * @param videoCapabilitiesSource the video capabilities source. Possible values include - * {@link Recorder#VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE} - * and - * {@link Recorder#VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES}. + * @param profilesProvider the encoder profiles provider. * @param cameraInfo the cameraInfo. - * @param videoEncoderInfoFinder the VideoEncoderInfo finder. - * @param videoCaptureType the video capture type. + * @param qualitySource the quality source. * @throws IllegalArgumentException if unable to get the capability information from the * CameraInfo or the videoCapabilitiesSource is not supported. */ - RecorderVideoCapabilities(@Recorder.VideoCapabilitiesSource int videoCapabilitiesSource, - @NonNull CameraInfoInternal cameraInfo, - @Recorder.VideoRecordingType int videoCaptureType, - VideoEncoderInfo.@NonNull Finder videoEncoderInfoFinder) { - checkArgument(videoCapabilitiesSource == VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE - || videoCapabilitiesSource == VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES, - "Not a supported video capabilities source: " + videoCapabilitiesSource); - - mQualitySource = videoCaptureType == VIDEO_RECORDING_TYPE_HIGH_SPEED - ? QUALITY_SOURCE_HIGH_SPEED : QUALITY_SOURCE_REGULAR; - mProfilesProvider = getEncoderProfilesProvider(videoCapabilitiesSource, cameraInfo, - videoEncoderInfoFinder, mQualitySource); + RecorderVideoCapabilities( + @NonNull EncoderProfilesProvider profilesProvider, + @Quality.QualitySource int qualitySource, + @NonNull CameraInfoInternal cameraInfo + ) { + mQualitySource = qualitySource; + mProfilesProvider = profilesProvider; // Group by dynamic range. for (DynamicRange dynamicRange : cameraInfo.getSupportedDynamicRanges()) { @@ -193,70 +152,6 @@ public boolean isStabilizationSupported() { : capabilities.findNearestHigherSupportedQualityFor(size); } - private static @NonNull EncoderProfilesProvider getEncoderProfilesProvider( - @Recorder.VideoCapabilitiesSource int videoCapabilitiesSource, - @NonNull CameraInfoInternal cameraInfo, - VideoEncoderInfo.@NonNull Finder videoEncoderInfoFinder, - @QualitySource int qualitySource) { - - EncoderProfilesProvider encoderProfilesProvider = cameraInfo.getEncoderProfilesProvider(); - - if (qualitySource == QUALITY_SOURCE_HIGH_SPEED) { - - if (!cameraInfo.isHighSpeedSupported()) { - return EncoderProfilesProvider.EMPTY; - } - - // TODO(b/399585664): explore high speed quality when video source is - // VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES - return encoderProfilesProvider; - } - - if (!containsSupportedQuality(encoderProfilesProvider, qualitySource)) { - Logger.w(TAG, "Camera EncoderProfilesProvider doesn't contain any supported Quality."); - // Limit maximum supported video resolution to 1080p(FHD). - // While 2160p(UHD) may be reported as supported by the Camera and MediaCodec APIs, - // testing on lab devices has shown that recording at this resolution is not always - // reliable. This aligns with the Android 5.1 CDD, which recommends 1080p as the - // supported resolution. - // See: https://source.android.com/static/docs/compatibility/5.1/android-5.1-cdd.pdf, - // 5.2. Video Encoding. - List targetQualities = Arrays.asList(FHD, HD, SD); - encoderProfilesProvider = new DefaultEncoderProfilesProvider(cameraInfo, - targetQualities, videoEncoderInfoFinder); - } - - Quirks deviceQuirks = DeviceQuirks.getAll(); - // Add extra supported quality. - encoderProfilesProvider = new QualityAddedEncoderProfilesProvider(encoderProfilesProvider, - deviceQuirks, cameraInfo, videoEncoderInfoFinder); - - if (videoCapabilitiesSource == VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES) { - encoderProfilesProvider = new QualityExploredEncoderProfilesProvider( - encoderProfilesProvider, - getSortedQualities(), - singleton(SDR), - cameraInfo.getSupportedResolutions(INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE), - videoEncoderInfoFinder); - } - - // Modify qualities' matching resolution to the value supported by camera. - encoderProfilesProvider = new QualityResolutionModifiedEncoderProfilesProvider( - encoderProfilesProvider, deviceQuirks); - - // Add backup HDR video information. In the initial version, only HLG10 profile is added. - if (isHlg10SupportedByCamera(cameraInfo)) { - encoderProfilesProvider = new BackupHdrProfileEncoderProfilesProvider( - encoderProfilesProvider, videoEncoderInfoFinder); - } - - // Filter out unsupported qualities. - encoderProfilesProvider = new QualityValidatedEncoderProfilesProvider( - encoderProfilesProvider, cameraInfo, deviceQuirks); - - return encoderProfilesProvider; - } - private @Nullable CapabilitiesByQuality getCapabilities(@NonNull DynamicRange dynamicRange) { if (dynamicRange.isFullySpecified()) { return mCapabilitiesMapForFullySpecifiedDynamicRange.get(dynamicRange); @@ -273,20 +168,6 @@ public boolean isStabilizationSupported() { } } - private static boolean isHlg10SupportedByCamera( - @NonNull CameraInfoInternal cameraInfoInternal) { - Set dynamicRanges = cameraInfoInternal.getSupportedDynamicRanges(); - for (DynamicRange dynamicRange : dynamicRanges) { - Integer encoding = dynamicRange.getEncoding(); - int bitDepth = dynamicRange.getBitDepth(); - if (encoding.equals(ENCODING_HLG) && bitDepth == DynamicRange.BIT_DEPTH_10_BIT) { - return true; - } - } - - return false; - } - private @Nullable CapabilitiesByQuality generateCapabilitiesForNonFullySpecifiedDynamicRange( @NonNull DynamicRange dynamicRange) { if (!DynamicRanges.canResolve(dynamicRange, getSupportedDynamicRanges())) { diff --git a/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilitiesFactory.kt b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilitiesFactory.kt new file mode 100644 index 0000000000000..1bc41297e5fd8 --- /dev/null +++ b/camera/camera-video/src/main/java/androidx/camera/video/RecorderVideoCapabilitiesFactory.kt @@ -0,0 +1,106 @@ +/* + * 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 androidx.camera.video + +import android.util.LruCache +import androidx.annotation.GuardedBy +import androidx.camera.core.CameraInfo +import androidx.camera.core.CameraSelector +import androidx.camera.core.impl.AdapterCameraInfo +import androidx.camera.core.impl.CameraInfoInternal +import androidx.camera.video.internal.encoder.VideoEncoderInfoImpl + +/** Factory for creating and caching [VideoCapabilities] instances. */ +internal object RecorderVideoCapabilitiesFactory { + @GuardedBy("capabilitiesCache") + private val capabilitiesCache = LruCache(16) + + /** Gets or creates [VideoCapabilities] for the given camera and configuration. */ + @JvmStatic + fun getCapabilities( + cameraInfo: CameraInfo, + @Recorder.VideoRecordingType videoRecordingType: Int, + @Recorder.VideoCapabilitiesSource videoCapabilitiesSource: Int, + ): VideoCapabilities { + if (shouldSkipCache(cameraInfo)) { + return createCapabilities(cameraInfo, videoRecordingType, videoCapabilitiesSource) + } + + val adapterInfo = cameraInfo as AdapterCameraInfo + val key = + CacheKey( + adapterInfo.cameraId, + adapterInfo.cameraConfig, + videoRecordingType, + videoCapabilitiesSource, + ) + + synchronized(capabilitiesCache) { + return capabilitiesCache.get(key) + ?: createCapabilities(cameraInfo, videoRecordingType, videoCapabilitiesSource) + .also { capabilitiesCache.put(key, it) } + } + } + + private fun createCapabilities( + cameraInfo: CameraInfo, + videoRecordingType: Int, + videoCapabilitiesSource: Int, + ): VideoCapabilities { + val cameraInfoInternal = cameraInfo as CameraInfoInternal + val qualitySource = + if (videoRecordingType == Recorder.VIDEO_RECORDING_TYPE_HIGH_SPEED) { + Quality.QUALITY_SOURCE_HIGH_SPEED + } else { + Quality.QUALITY_SOURCE_REGULAR + } + + val resolvedProvider = + EncoderProfilesProviderResolver.resolve( + cameraInfo = cameraInfoInternal, + videoCapabilitiesSource = videoCapabilitiesSource, + qualitySource = qualitySource, + videoEncoderInfoFinder = VideoEncoderInfoImpl.FINDER, + ) + + return RecorderVideoCapabilities(resolvedProvider, videoRecordingType, cameraInfoInternal) + } + + /** + * Checks whether the video capabilities for a given camera should be cached. + * + * Caching is skipped for external cameras or cameras with an unknown lens facing, as their + * properties may not be stable across device reboots or during camera hot-plugging. + */ + private fun shouldSkipCache(cameraInfo: CameraInfo): Boolean { + return if (cameraInfo is AdapterCameraInfo) { + cameraInfo.isExternalCamera || + cameraInfo.lensFacing == CameraSelector.LENS_FACING_UNKNOWN + } else { + // If we can't determine the camera properties (e.g., not an AdapterCameraInfo), + // it's safer to skip caching. + true + } + } + + private data class CacheKey( + val cameraId: String, + val cameraConfig: Any, + val videoRecordingType: Int, + val videoCapabilitiesSource: Int, + ) +} diff --git a/camera/camera-video/src/test/java/androidx/camera/video/EncoderProfilesProviderResolverTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/EncoderProfilesProviderResolverTest.kt new file mode 100644 index 0000000000000..c6c5cd7b217fc --- /dev/null +++ b/camera/camera-video/src/test/java/androidx/camera/video/EncoderProfilesProviderResolverTest.kt @@ -0,0 +1,166 @@ +/* + * 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 androidx.camera.video + +import android.media.CamcorderProfile.QUALITY_1080P +import android.media.CamcorderProfile.QUALITY_2160P +import android.media.CamcorderProfile.QUALITY_HIGH_SPEED_HIGH +import android.media.EncoderProfiles.VideoProfile +import androidx.camera.core.DynamicRange +import androidx.camera.core.impl.EncoderProfilesProvider +import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE +import androidx.camera.testing.fakes.FakeCameraInfoInternal +import androidx.camera.testing.impl.EncoderProfilesUtil.PROFILES_1080P +import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_1080P +import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_2160P +import androidx.camera.testing.impl.fakes.FakeEncoderProfilesProvider +import androidx.camera.testing.impl.fakes.FakeVideoEncoderInfo +import androidx.camera.video.Quality.QUALITY_SOURCE_HIGH_SPEED +import androidx.camera.video.Quality.QUALITY_SOURCE_REGULAR +import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE +import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES +import androidx.camera.video.internal.encoder.VideoEncoderInfo +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.internal.DoNotInstrument + +@RunWith(RobolectricTestRunner::class) +@DoNotInstrument +@Config(sdk = [Config.ALL_SDKS]) +class EncoderProfilesProviderResolverTest { + + private val videoEncoderInfoFinder = VideoEncoderInfo.Finder { FakeVideoEncoderInfo() } + + @Test + fun resolve_highSpeed_returnsProviderIfSupported() { + val cameraInfo = + FakeCameraInfoInternal().apply { + isHighSpeedSupported = true + encoderProfilesProvider = + FakeEncoderProfilesProvider.Builder() + .add(QUALITY_HIGH_SPEED_HIGH, PROFILES_1080P) + .build() + } + + val provider = + EncoderProfilesProviderResolver.resolve( + cameraInfo, + VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + QUALITY_SOURCE_HIGH_SPEED, + videoEncoderInfoFinder, + ) + + assertThat(provider).isSameInstanceAs(cameraInfo.encoderProfilesProvider) + } + + @Test + fun resolve_highSpeed_returnsEmptyIfNotSupported() { + val cameraInfo = + FakeCameraInfoInternal().apply { + isHighSpeedSupported = false + encoderProfilesProvider = + FakeEncoderProfilesProvider.Builder() + .add(QUALITY_HIGH_SPEED_HIGH, PROFILES_1080P) + .build() + } + + val provider = + EncoderProfilesProviderResolver.resolve( + cameraInfo, + VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + QUALITY_SOURCE_HIGH_SPEED, + videoEncoderInfoFinder, + ) + + assertThat(provider).isSameInstanceAs(EncoderProfilesProvider.EMPTY) + } + + @Test + fun resolve_noSupportedQuality_usesDefaultProvider() { + val cameraInfo = + FakeCameraInfoInternal().apply { + encoderProfilesProvider = EncoderProfilesProvider.EMPTY + setSupportedResolutions( + INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, + listOf(RESOLUTION_1080P), + ) + } + + val provider = + EncoderProfilesProviderResolver.resolve( + cameraInfo, + VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + QUALITY_SOURCE_REGULAR, + videoEncoderInfoFinder, + ) + + // Assert: 1080p is added. + assertThat(provider.hasProfile(QUALITY_1080P)).isTrue() + } + + @Test + fun resolve_codecCapabilitiesSource_usesQualityExploredProvider() { + val cameraInfo = + FakeCameraInfoInternal().apply { + encoderProfilesProvider = + FakeEncoderProfilesProvider.Builder().add(QUALITY_1080P, PROFILES_1080P).build() + setSupportedResolutions( + INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE, + listOf(RESOLUTION_1080P, RESOLUTION_2160P), + ) + } + + val provider = + EncoderProfilesProviderResolver.resolve( + cameraInfo, + VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES, + QUALITY_SOURCE_REGULAR, + videoEncoderInfoFinder, + ) + + // Assert: 2160P is explored. + assertThat(provider.hasProfile(QUALITY_2160P)).isTrue() + } + + @Test + fun resolve_hlg10Supported_usesBackupHdrProvider() { + val cameraInfo = + FakeCameraInfoInternal().apply { + encoderProfilesProvider = + FakeEncoderProfilesProvider.Builder().add(QUALITY_1080P, PROFILES_1080P).build() + supportedDynamicRanges = setOf(DynamicRange.HLG_10_BIT) + } + + val provider = + EncoderProfilesProviderResolver.resolve( + cameraInfo, + VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + QUALITY_SOURCE_REGULAR, + videoEncoderInfoFinder, + ) + + assertThat( + provider.getAll(QUALITY_1080P)!!.videoProfiles.any { + it.hdrFormat == VideoProfile.HDR_HLG + } + ) + .isTrue() + } +} diff --git a/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesFactoryTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesFactoryTest.kt new file mode 100644 index 0000000000000..087657403211b --- /dev/null +++ b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesFactoryTest.kt @@ -0,0 +1,235 @@ +/* + * 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 androidx.camera.video + +import androidx.camera.core.CameraSelector +import androidx.camera.core.impl.AdapterCameraInfo +import androidx.camera.testing.fakes.FakeCameraInfoInternal +import androidx.camera.testing.impl.fakes.FakeCameraConfig +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.annotation.internal.DoNotInstrument + +@RunWith(RobolectricTestRunner::class) +@DoNotInstrument +@Config(sdk = [Config.ALL_SDKS]) +class RecorderVideoCapabilitiesFactoryTest { + + @Test + fun getCapabilities_returnsCachedInstanceForSameCamera() { + val cameraInfo = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_returnsNewInstanceForDifferentCamera() { + val cameraInfo1 = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + FakeCameraConfig(), + ) + val cameraInfo2 = + AdapterCameraInfo( + FakeCameraInfoInternal("1", CameraSelector.LENS_FACING_BACK), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo1, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo2, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_returnsCachedInstanceForDifferentCameraInfoWithSameIdAndConfig() { + val cameraConfig = FakeCameraConfig() + val cameraInfo1 = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + cameraConfig, + ) + val cameraInfo2 = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + cameraConfig, + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo1, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo2, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_returnsNewInstanceForDifferentRecordingType() { + val cameraInfo = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_HIGH_SPEED, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_returnsNewInstanceForDifferentSource() { + val cameraInfo = + AdapterCameraInfo( + FakeCameraInfoInternal("0", CameraSelector.LENS_FACING_BACK), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES, + ) + + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_doesNotCacheForExternalCamera() { + val cameraInfo = + AdapterCameraInfo( + FakeCameraInfoInternal("external", CameraSelector.LENS_FACING_EXTERNAL), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_doesNotCacheForUnknownLensFacingCamera() { + val cameraInfo = + AdapterCameraInfo( + FakeCameraInfoInternal("unknown", CameraSelector.LENS_FACING_UNKNOWN), + FakeCameraConfig(), + ) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } + + @Test + fun getCapabilities_returnsDifferentInstancesForDifferentCameraConfigs() { + val cameraInfo1 = AdapterCameraInfo(FakeCameraInfoInternal("0"), FakeCameraConfig()) + val cameraInfo2 = AdapterCameraInfo(FakeCameraInfoInternal("0"), FakeCameraConfig()) + + val capabilities1 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo1, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + val capabilities2 = + RecorderVideoCapabilitiesFactory.getCapabilities( + cameraInfo2, + Recorder.VIDEO_RECORDING_TYPE_REGULAR, + Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, + ) + + // Assert + assertThat(capabilities1).isNotSameInstanceAs(capabilities2) + } +} diff --git a/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt index bc8d50723e721..0331c82e8da54 100644 --- a/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt +++ b/camera/camera-video/src/test/java/androidx/camera/video/RecorderVideoCapabilitiesTest.kt @@ -37,7 +37,6 @@ import androidx.camera.core.DynamicRange.HDR_UNSPECIFIED_10_BIT import androidx.camera.core.DynamicRange.HLG_10_BIT import androidx.camera.core.DynamicRange.SDR import androidx.camera.core.DynamicRange.UNSPECIFIED -import androidx.camera.core.impl.EncoderProfilesProvider import androidx.camera.core.impl.ImageFormatConstants.INTERNAL_DEFINED_IMAGE_FORMAT_PRIVATE import androidx.camera.testing.fakes.FakeCameraInfoInternal import androidx.camera.testing.impl.EncoderProfilesUtil.PROFILES_2160P @@ -46,6 +45,7 @@ import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_1080P import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_2160P import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_480P import androidx.camera.testing.impl.EncoderProfilesUtil.RESOLUTION_720P +import androidx.camera.testing.impl.EncoderProfilesUtil.createFakeEncoderProfilesProxy import androidx.camera.testing.impl.EncoderProfilesUtil.createFakeHighSpeedEncoderProfilesProxy import androidx.camera.testing.impl.FrameRateUtil.FPS_120_120 import androidx.camera.testing.impl.FrameRateUtil.FPS_240 @@ -56,22 +56,18 @@ import androidx.camera.testing.impl.FrameRateUtil.FPS_30_480 import androidx.camera.testing.impl.FrameRateUtil.FPS_480 import androidx.camera.testing.impl.FrameRateUtil.FPS_480_480 import androidx.camera.testing.impl.fakes.FakeEncoderProfilesProvider -import androidx.camera.testing.impl.fakes.FakeVideoEncoderInfo import androidx.camera.video.Quality.FHD import androidx.camera.video.Quality.HD import androidx.camera.video.Quality.HIGHEST import androidx.camera.video.Quality.LOWEST +import androidx.camera.video.Quality.QUALITY_SOURCE_HIGH_SPEED +import androidx.camera.video.Quality.QUALITY_SOURCE_REGULAR import androidx.camera.video.Quality.SD import androidx.camera.video.Quality.UHD -import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE -import androidx.camera.video.Recorder.VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES -import androidx.camera.video.Recorder.VIDEO_RECORDING_TYPE_HIGH_SPEED -import androidx.camera.video.Recorder.VIDEO_RECORDING_TYPE_REGULAR import androidx.camera.video.internal.VideoValidatedEncoderProfilesProxy import androidx.core.util.component1 import androidx.core.util.component2 import com.google.common.truth.Truth.assertThat -import org.junit.Assume.assumeTrue import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner @@ -86,16 +82,15 @@ private val DOLBY_VISION_UNSPECIFIED = DynamicRange(ENCODING_DOLBY_VISION, BIT_D @RunWith(ParameterizedRobolectricTestRunner::class) @DoNotInstrument @Config(sdk = [Config.ALL_SDKS]) -class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { +class RecorderVideoCapabilitiesTest(private val qualitySource: Int) { companion object { @JvmStatic - @ParameterizedRobolectricTestRunner.Parameters(name = "videoCaptureType={0}") - fun data() = - listOf(arrayOf(VIDEO_RECORDING_TYPE_REGULAR), arrayOf(VIDEO_RECORDING_TYPE_HIGH_SPEED)) + @ParameterizedRobolectricTestRunner.Parameters(name = "qualitySource={0}") + fun data() = listOf(arrayOf(QUALITY_SOURCE_REGULAR), arrayOf(QUALITY_SOURCE_HIGH_SPEED)) } - private val isHighSpeed = videoCaptureType == VIDEO_RECORDING_TYPE_HIGH_SPEED + private val isHighSpeed = qualitySource == QUALITY_SOURCE_HIGH_SPEED private val defaultProfilesProvider = FakeEncoderProfilesProvider.Builder() @@ -119,11 +114,20 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { add(QUALITY_HIGH_SPEED_720P, profile720p480fpsSdrHlg) // HD (720p) add(QUALITY_HIGH_SPEED_LOW, profile720p480fpsSdrHlg) } else { - // HLG profiles will be generated by BackupHdrProfileEncoderProfilesProvider - add(QUALITY_HIGH, PROFILES_2160P) // UHD (2160p) per above definition - add(QUALITY_2160P, PROFILES_2160P) // UHD (2160p) - add(QUALITY_720P, PROFILES_720P) // HD (720p) - add(QUALITY_LOW, PROFILES_720P) // HD (720p) per above definition + val profile2160pSdrHlg = + createFakeEncoderProfilesProxy( + videoResolution = RESOLUTION_2160P, + dynamicRanges = setOf(SDR, HLG_10_BIT), + ) + val profile720pSdrHlg = + createFakeEncoderProfilesProxy( + videoResolution = RESOLUTION_720P, + dynamicRanges = setOf(SDR, HLG_10_BIT), + ) + add(QUALITY_HIGH, profile2160pSdrHlg) // UHD (2160p) per above definition + add(QUALITY_2160P, profile2160pSdrHlg) // UHD (2160p) + add(QUALITY_720P, profile720pSdrHlg) // HD (720p) + add(QUALITY_LOW, profile720pSdrHlg) // HD (720p) per above definition } .build() } @@ -182,13 +186,7 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { else PROFILES_720P ) private val videoCapabilities = - RecorderVideoCapabilities( - VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, - cameraInfo, - videoCaptureType, - ) { - FakeVideoEncoderInfo() - } + RecorderVideoCapabilities(cameraInfo.encoderProfilesProvider, qualitySource, cameraInfo) @Test fun canGetSupportedDynamicRanges() { @@ -265,7 +263,7 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { } @Test - fun isQualitySupported_hlg10WithBackupProfile() { + fun isQualitySupported_hlg10() { assertThat(videoCapabilities.isQualitySupported(HIGHEST, HLG_10_BIT)).isTrue() assertThat(videoCapabilities.isQualitySupported(LOWEST, HLG_10_BIT)).isTrue() assertThat(videoCapabilities.isQualitySupported(UHD, HLG_10_BIT)).isTrue() @@ -275,7 +273,7 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { } @Test - fun isQualitySupported_hdrUnspecified10BitWithBackupProfile() { + fun isQualitySupported_hdrUnspecified10Bit() { assertThat(videoCapabilities.isQualitySupported(HIGHEST, HDR_UNSPECIFIED_10_BIT)).isTrue() assertThat(videoCapabilities.isQualitySupported(LOWEST, HDR_UNSPECIFIED_10_BIT)).isTrue() assertThat(videoCapabilities.isQualitySupported(UHD, HDR_UNSPECIFIED_10_BIT)).isTrue() @@ -295,7 +293,7 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { } @Test - fun canGetNonNullHdrUnspecifiedBackupProfile_whenSdrProfileExisted() { + fun canGetNonNullHdrUnspecified() { assertThat(videoCapabilities.getProfiles(HIGHEST, HDR_UNSPECIFIED_10_BIT)).isNotNull() assertThat(videoCapabilities.getProfiles(LOWEST, HDR_UNSPECIFIED_10_BIT)).isNotNull() assertThat(videoCapabilities.getProfiles(UHD, HDR_UNSPECIFIED_10_BIT)).isNotNull() @@ -305,7 +303,7 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { } @Test - fun canGetNonNullHlg10BackupProfile_whenSdrProfileExisted() { + fun canGetNonNullHlg10() { assertThat(videoCapabilities.getProfiles(HIGHEST, HLG_10_BIT)).isNotNull() assertThat(videoCapabilities.getProfiles(LOWEST, HLG_10_BIT)).isNotNull() assertThat(videoCapabilities.getProfiles(UHD, HLG_10_BIT)).isNotNull() @@ -400,49 +398,4 @@ class RecorderVideoCapabilitiesTest(private val videoCaptureType: Int) { ) .isEqualTo(validatedProfiles720p) } - - @Test - fun createBySourceCodecCapabilities_additionalQualitiesAreSupported() { - // TODO(b/399585664): Remove this assumption when high speed quality exploration is - // supported. - assumeTrue("High speed mode does not yet support quality exploration", !isHighSpeed) - - val codecVideoCapabilities = - RecorderVideoCapabilities( - VIDEO_CAPABILITIES_SOURCE_CODEC_CAPABILITIES, - cameraInfo, - videoCaptureType, - ) { - FakeVideoEncoderInfo() - } - - // FHD and SD should become supported. - assertThat(videoCapabilities.isQualitySupported(FHD, SDR)).isFalse() - assertThat(videoCapabilities.isQualitySupported(SD, SDR)).isFalse() - assertThat(codecVideoCapabilities.isQualitySupported(FHD, SDR)).isTrue() - assertThat(codecVideoCapabilities.isQualitySupported(SD, SDR)).isTrue() - } - - @Test - fun noSupportedQuality_shouldCreateDefaultEncoderProfilesProvider() { - assumeTrue("High speed mode doesn't adopt DefaultEncoderProfilesProvider", !isHighSpeed) - - cameraInfo.encoderProfilesProvider = EncoderProfilesProvider.EMPTY - val codecVideoCapabilities = - RecorderVideoCapabilities( - VIDEO_CAPABILITIES_SOURCE_CAMCORDER_PROFILE, - cameraInfo, - videoCaptureType, - ) { - FakeVideoEncoderInfo() - } - - assertThat(codecVideoCapabilities.isQualitySupported(HIGHEST, SDR)).isTrue() - assertThat(codecVideoCapabilities.isQualitySupported(LOWEST, SDR)).isTrue() - // The target quality is [FHD, HD, SD] - assertThat(codecVideoCapabilities.isQualitySupported(UHD, SDR)).isFalse() - assertThat(codecVideoCapabilities.isQualitySupported(FHD, SDR)).isTrue() - assertThat(codecVideoCapabilities.isQualitySupported(HD, SDR)).isTrue() - assertThat(codecVideoCapabilities.isQualitySupported(SD, SDR)).isTrue() - } } diff --git a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java index 78485919ba4d5..36f4031ac2881 100644 --- a/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java +++ b/car/app/app-samples/showcase/common/src/main/java/androidx/car/app/sample/showcase/common/screens/userinteractions/RequestPermissionScreen.java @@ -190,6 +190,13 @@ private List findMissingPermissions() throws PackageManager.NameNotFound continue; } + if (!isPermissionSupportedBySdk(permission)) { + Log.d( + TAG, String.format(Locale.US, + "Permission ignored (not supported by SDK): %s", permission)); + continue; + } + Log.d(TAG, String.format(Locale.US, "Found missing permission: %s", permission)); missingPermissions.add(permission); } @@ -213,6 +220,22 @@ private boolean isPermissionGranted(String permission) { return true; } + /** + * Checks if a permission is supported by the current Android SDK version by querying the + * PackageManager. + */ + private boolean isPermissionSupportedBySdk(String permission) { + try { + getCarContext().getPackageManager().getPermissionInfo(permission, 0); + } catch (PackageManager.NameNotFoundException e) { + // Permission is not supported by the SDK + return false; + } + + // Permission is supported by the SDK + return true; + } + private boolean needsLocationPermission() { LocationManager locationManager = (LocationManager) getCarContext().getSystemService(Context.LOCATION_SERVICE); diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt index a63458f1601ab..5992c7f07bf0c 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/EditablePdfViewerFragment.kt @@ -240,7 +240,7 @@ public open class EditablePdfViewerFragment : PdfViewerFragment { object : OnAnnotationSelectedListener { override fun onAnnotationSelected(keyedPdfAnnotation: KeyedPdfAnnotation) { if (documentViewModel.drawingMode.value == AnnotationDrawingMode.EraserMode) { - // TODO: (b/473955799) Call removeAnnotation on EditableDocumentViewModel. + documentViewModel.removeAnnotation(keyedPdfAnnotation.key) } } } diff --git a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/PdfContentLayoutTouchListener.kt b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/PdfContentLayoutTouchListener.kt index 4ade46acbdd56..ce98d9ec6bdcc 100644 --- a/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/PdfContentLayoutTouchListener.kt +++ b/pdf/pdf-ink/src/main/kotlin/androidx/pdf/ink/PdfContentLayoutTouchListener.kt @@ -124,36 +124,19 @@ internal class PdfContentLayoutTouchListener( } val primaryPointerIndex = event.findPointerIndex(primaryPointerId) - if (isSingleTouchCommitted && currentDispatcher == annotationsTouchEventDispatcher) { - if (primaryPointerIndex != -1) { - val x = event.getX(primaryPointerIndex) - val y = event.getY(primaryPointerIndex) - val singlePointerMove = - MotionEvent.obtain( - event.downTime, - event.eventTime, - event.action, - x, - y, - event.metaState, - ) - currentDispatcher?.dispatchTouchEvent(singlePointerMove) - singlePointerMove.recycle() - } - } else { - if ( - currentDispatcher == annotationsTouchEventDispatcher && - !isSingleTouchCommitted && - primaryPointerIndex != -1 - ) { - val dx = event.getX(primaryPointerIndex) - downX - val dy = event.getY(primaryPointerIndex) - downY - if (dx * dx + dy * dy > touchSlop * touchSlop) { - isSingleTouchCommitted = true - } + if (primaryPointerIndex == MotionEvent.INVALID_POINTER_ID) { + return + } + + if (currentDispatcher == annotationsTouchEventDispatcher && !isSingleTouchCommitted) { + val dx = event.getX(primaryPointerIndex) - downX + val dy = event.getY(primaryPointerIndex) - downY + if (dx * dx + dy * dy > touchSlop * touchSlop) { + isSingleTouchCommitted = true } - currentDispatcher?.dispatchTouchEvent(event) } + + currentDispatcher?.dispatchTouchEvent(event) } private fun handlePointerUp(event: MotionEvent) { diff --git a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/annotation/AnnotationSelectionTouchHandler.kt b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/annotation/AnnotationSelectionTouchHandler.kt index 887b8e06c797e..8489d52067ce4 100644 --- a/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/annotation/AnnotationSelectionTouchHandler.kt +++ b/pdf/pdf-viewer/src/main/kotlin/androidx/pdf/annotation/AnnotationSelectionTouchHandler.kt @@ -55,10 +55,7 @@ internal class AnnotationSelectionTouchHandler() { val selectedAnnotation = findAnnotationAtPoint(annotationsView, pageInfo, event) if (selectedAnnotation != null) { - // TODO(b/470857248): Replace with actual keyedPdfAnnotation from - // annotationsView. - val keyedPdfAnnotation = KeyedPdfAnnotation(KEY, selectedAnnotation) - onAnnotationSelectedListener?.onAnnotationSelected(keyedPdfAnnotation) + onAnnotationSelectedListener?.onAnnotationSelected(selectedAnnotation) return true } false @@ -72,7 +69,7 @@ internal class AnnotationSelectionTouchHandler() { annotationsView: AnnotationsView, pageInfo: PageInfoProvider.PageInfo, event: MotionEvent, - ): PdfAnnotation? { + ): KeyedPdfAnnotation? { // Use the system's touch slop for a density-aware tolerance. val touchSlop: Int = ViewConfiguration.get(annotationsView.context).scaledTouchSlop @@ -89,14 +86,12 @@ internal class AnnotationSelectionTouchHandler() { pageInfo.viewToPageTransform.mapRect(touchRectPdf, touchRectView) val touchRegion = touchRectPdf.toRegion() - val annotations = - annotationsView.annotations.get(pageInfo.pageNum)?.keyedAnnotations?.map { - it.annotation - } ?: return null + val keyedPdfAnnotations = + annotationsView.annotations.get(pageInfo.pageNum)?.keyedAnnotations ?: return null // Iterate in reverse Z-order to find the top-most annotation. - return annotations.asReversed().firstOrNull { annotation -> - isAnnotationHit(annotation, touchRegion, touchRectPdf) + return keyedPdfAnnotations.asReversed().firstOrNull { keyedPdfAnnotation -> + isAnnotationHit(keyedPdfAnnotation.annotation, touchRegion, touchRectPdf) } } @@ -147,8 +142,4 @@ internal class AnnotationSelectionTouchHandler() { ceil(bottom).toInt(), ) } - - private companion object { - const val KEY = "key" - } }