Add audio enable/disable feature
Relnote: "Add AudioConfig class to handle the audio related setting
while recording video. The @RequiresPermission annotation is moved
from startRecording functions to AudioConfig to avoid unnecessary
permission requests for the cases that audio is not needed."
Bug: 209528390
Test: run CameraControllerTest, VideoCaptureDeviceTest
& CameraControllerFragmentTest
Change-Id: I28755ca547aa91ee5d4de3440ab9e691c9a856a7
diff --git a/camera/camera-view/api/public_plus_experimental_current.txt b/camera/camera-view/api/public_plus_experimental_current.txt
index 0f8b2c0..a371cb4 100644
--- a/camera/camera-view/api/public_plus_experimental_current.txt
+++ b/camera/camera-view/api/public_plus_experimental_current.txt
@@ -45,9 +45,9 @@
method @MainThread public void setTapToFocusEnabled(boolean);
method @MainThread @androidx.camera.view.video.ExperimentalVideo public void setVideoCaptureTargetQuality(androidx.camera.video.Quality?);
method @MainThread public com.google.common.util.concurrent.ListenableFuture<java.lang.Void!> setZoomRatio(float);
- method @MainThread @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
- method @MainThread @RequiresApi(26) @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
- method @MainThread @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread @RequiresApi(26) @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.FileDescriptorOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
+ method @MainThread @androidx.camera.view.video.ExperimentalVideo public androidx.camera.video.Recording startRecording(androidx.camera.video.MediaStoreOutputOptions, androidx.camera.view.video.AudioConfig, java.util.concurrent.Executor, androidx.core.util.Consumer<androidx.camera.video.VideoRecordEvent!>);
method @MainThread public void takePicture(androidx.camera.core.ImageCapture.OutputFileOptions, java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageSavedCallback);
method @MainThread public void takePicture(java.util.concurrent.Executor, androidx.camera.core.ImageCapture.OnImageCapturedCallback);
field public static final int COORDINATE_SYSTEM_VIEW_REFERENCED = 1; // 0x1
@@ -161,6 +161,12 @@
package androidx.camera.view.video {
+ @RequiresApi(21) @androidx.camera.view.video.ExperimentalVideo public class AudioConfig {
+ method @RequiresPermission(android.Manifest.permission.RECORD_AUDIO) public static androidx.camera.view.video.AudioConfig create(boolean);
+ method public boolean getAudioEnabled();
+ field public static final androidx.camera.view.video.AudioConfig AUDIO_DISABLED;
+ }
+
@RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalVideo {
}
diff --git a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
index 3d0e7a1..d037ab4 100644
--- a/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
+++ b/camera/camera-view/src/androidTest/java/androidx/camera/view/VideoCaptureDeviceTest.kt
@@ -43,6 +43,7 @@
import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
import androidx.camera.view.CameraController.VIDEO_CAPTURE
+import androidx.camera.view.video.AudioConfig
import androidx.core.util.Consumer
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.rules.ActivityScenarioRule
@@ -135,6 +136,8 @@
private val instrumentation = InstrumentationRegistry.getInstrumentation()
private val context: Context = ApplicationProvider.getApplicationContext()
+ private val audioEnabled = AudioConfig.create(true)
+ private val audioDisabled = AudioConfig.AUDIO_DISABLED
private lateinit var previewView: PreviewView
private lateinit var lifecycleOwner: FakeLifecycleOwner
private lateinit var cameraController: LifecycleCameraController
@@ -202,7 +205,7 @@
val outputOptions = createMediaStoreOutputOptions(resolver)
// Act.
- recordVideoCompletely(outputOptions)
+ recordVideoCompletely(outputOptions, audioEnabled)
// Verify.
val uri = finalize.outputResults.outputUri
@@ -222,7 +225,7 @@
val outputOptions = FileDescriptorOutputOptions.Builder(fileDescriptor).build()
// Act.
- recordVideoCompletely(outputOptions)
+ recordVideoCompletely(outputOptions, audioEnabled)
// Verify.
val uri = Uri.fromFile(file)
@@ -240,7 +243,7 @@
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoCompletely(outputOptions)
+ recordVideoCompletely(outputOptions, audioEnabled)
// Verify.
val uri = Uri.fromFile(file)
@@ -252,13 +255,31 @@
}
@Test
+ fun canRecordToFile_withoutAudio_whenAudioDisabled() {
+ // Arrange.
+ val file = createTempFile()
+ val outputOptions = FileOutputOptions.Builder(file).build()
+
+ // Act.
+ recordVideoCompletely(outputOptions, audioDisabled)
+
+ // Verify.
+ val uri = Uri.fromFile(file)
+ checkFileOnlyHasVideo(uri)
+ assertThat(finalize.outputResults.outputUri).isEqualTo(uri)
+
+ // Cleanup.
+ file.delete()
+ }
+
+ @Test
fun canRecordToFile_whenLifecycleStops() {
// Arrange.
val file = createTempFile()
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioEnabled) {
instrumentation.runOnMainSync {
lifecycleOwner.pauseAndStop()
}
@@ -281,7 +302,7 @@
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioEnabled) {
instrumentation.runOnMainSync {
cameraController.videoCaptureTargetQuality = nextQuality.get()
}
@@ -304,7 +325,7 @@
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioEnabled) {
instrumentation.runOnMainSync {
cameraController.setEnabledUseCases(IMAGE_ANALYSIS)
}
@@ -330,7 +351,7 @@
// Pre Act.
latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
- recordVideo(outputOptions1)
+ recordVideo(outputOptions1, audioEnabled)
instrumentation.runOnMainSync {
activeRecording.stop()
assertThat(cameraController.isRecording).isFalse()
@@ -338,7 +359,7 @@
// Act.
instrumentation.runOnMainSync {
- startRecording(outputOptions2)
+ startRecording(outputOptions2, audioEnabled)
assertThat(cameraController.isRecording).isTrue()
}
@@ -381,7 +402,7 @@
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioEnabled) {
instrumentation.runOnMainSync {
activeRecording.pause()
}
@@ -413,7 +434,7 @@
val outputOptions = FileOutputOptions.Builder(file).build()
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioEnabled) {
instrumentation.runOnMainSync {
activeRecording.pause()
}
@@ -447,11 +468,12 @@
val outputOptions2 = FileOutputOptions.Builder(file2).build()
// Act.
- recordVideoWithInterruptAction(outputOptions1) {
+ recordVideoWithInterruptAction(outputOptions1, audioEnabled) {
instrumentation.runOnMainSync {
assertThrows(java.lang.IllegalStateException::class.java) {
activeRecording = cameraController.startRecording(
outputOptions2,
+ audioEnabled,
CameraXExecutors.directExecutor()
) {}
}
@@ -511,9 +533,9 @@
.build()
}
- private fun recordVideoCompletely(outputOptions: OutputOptions) {
+ private fun recordVideoCompletely(outputOptions: OutputOptions, audioConfig: AudioConfig) {
// Act.
- recordVideoWithInterruptAction(outputOptions) {
+ recordVideoWithInterruptAction(outputOptions, audioConfig) {
instrumentation.runOnMainSync {
activeRecording.stop()
}
@@ -525,13 +547,14 @@
private fun recordVideoWithInterruptAction(
outputOptions: OutputOptions,
+ audioConfig: AudioConfig,
runInterruptAction: () -> Unit
) {
// Arrange.
latchForVideoSaved = CountDownLatch(VIDEO_SAVED_COUNT_DOWN)
// Act.
- recordVideo(outputOptions)
+ recordVideo(outputOptions, audioConfig)
runInterruptAction()
// Verify.
@@ -543,14 +566,14 @@
}
}
- private fun recordVideo(outputOptions: OutputOptions) {
+ private fun recordVideo(outputOptions: OutputOptions, audioConfig: AudioConfig) {
// Arrange.
latchForVideoStarted = CountDownLatch(VIDEO_STARTED_COUNT_DOWN)
latchForVideoRecording = CountDownLatch(VIDEO_RECORDING_COUNT_DOWN)
// Act.
instrumentation.runOnMainSync {
- startRecording(outputOptions)
+ startRecording(outputOptions, audioConfig)
assertThat(cameraController.isRecording).isTrue()
}
@@ -562,10 +585,11 @@
}
@MainThread
- private fun startRecording(outputOptions: OutputOptions) {
+ private fun startRecording(outputOptions: OutputOptions, audioConfig: AudioConfig) {
if (outputOptions is FileOutputOptions) {
activeRecording = cameraController.startRecording(
outputOptions,
+ audioConfig,
CameraXExecutors.directExecutor(),
videoRecordEventListener
)
@@ -573,6 +597,7 @@
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
activeRecording = cameraController.startRecording(
outputOptions,
+ audioConfig,
CameraXExecutors.directExecutor(),
videoRecordEventListener
)
@@ -584,6 +609,7 @@
} else if (outputOptions is MediaStoreOutputOptions) {
activeRecording = cameraController.startRecording(
outputOptions,
+ audioConfig,
CameraXExecutors.directExecutor(),
videoRecordEventListener
)
@@ -592,9 +618,14 @@
}
}
+ private fun checkFileOnlyHasVideo(uri: Uri) {
+ checkFileHasVideo(uri)
+ checkFileHasAudio(uri, false)
+ }
+
private fun checkFileHasAudioAndVideo(uri: Uri) {
checkFileHasVideo(uri)
- checkFileHasAudio(uri)
+ checkFileHasAudio(uri, true)
}
private fun checkFileHasVideo(uri: Uri) {
@@ -606,13 +637,13 @@
}
}
- private fun checkFileHasAudio(uri: Uri) {
+ private fun checkFileHasAudio(uri: Uri, hasAudio: Boolean) {
val mediaRetriever = MediaMetadataRetriever()
mediaRetriever.apply {
setDataSource(context, uri)
val value = extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO)
- assertThat(value).isEqualTo("yes")
+ assertThat(value).isEqualTo(if (hasAudio) "yes" else null)
}
}
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
index 11cf580..3d95114 100644
--- a/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
+++ b/camera/camera-view/src/main/java/androidx/camera/view/CameraController.java
@@ -85,7 +85,9 @@
import androidx.camera.video.VideoCapture;
import androidx.camera.video.VideoRecordEvent;
import androidx.camera.view.transform.OutputTransform;
+import androidx.camera.view.video.AudioConfig;
import androidx.camera.view.video.ExperimentalVideo;
+import androidx.core.content.PermissionChecker;
import androidx.core.util.Consumer;
import androidx.core.util.Preconditions;
import androidx.lifecycle.LiveData;
@@ -1158,26 +1160,28 @@
* will be the first event sent to the provided listener, and information about the error can
* be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
*
- * <p> Recording requires the {@link android.Manifest.permission#RECORD_AUDIO} permission;
- * without it, starting a recording will fail with a {@link SecurityException}.
+ * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+ * permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
+ * @param audioConfig the configuration of audio.
* @param executor the executor that the event listener will be run on.
* @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the {@link android.Manifest.permission#RECORD_AUDIO}
- * permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
*/
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+ @SuppressLint("MissingPermission")
@ExperimentalVideo
@MainThread
@NonNull
public Recording startRecording(
@NonNull FileOutputOptions outputOptions,
+ @NonNull AudioConfig audioConfig,
@NonNull Executor executor,
@NonNull Consumer<VideoRecordEvent> listener) {
- return startRecordingInternal(outputOptions, executor, listener);
+ return startRecordingInternal(outputOptions, audioConfig, executor, listener);
}
/**
@@ -1196,27 +1200,29 @@
* will be the first event sent to the provided listener, and information about the error can
* be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
*
- * <p> Recording requires the {@link android.Manifest.permission#RECORD_AUDIO} permission;
- * without it, starting a recording will fail with a {@link SecurityException}.
+ * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+ * permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
+ * @param audioConfig the configuration of audio.
* @param executor the executor that the event listener will be run on.
* @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the {@link android.Manifest.permission#RECORD_AUDIO}
- * permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
*/
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+ @SuppressLint("MissingPermission")
@ExperimentalVideo
@RequiresApi(26)
@MainThread
@NonNull
public Recording startRecording(
@NonNull FileDescriptorOutputOptions outputOptions,
+ @NonNull AudioConfig audioConfig,
@NonNull Executor executor,
@NonNull Consumer<VideoRecordEvent> listener) {
- return startRecordingInternal(outputOptions, executor, listener);
+ return startRecordingInternal(outputOptions, audioConfig, executor, listener);
}
/**
@@ -1232,26 +1238,28 @@
* will be the first event sent to the provided listener, and information about the error can
* be found in that event's {@link VideoRecordEvent.Finalize#getError()} method.
*
- * <p> Recording requires the {@link android.Manifest.permission#RECORD_AUDIO} permission;
- * without it, starting a recording will fail with a {@link SecurityException}.
+ * <p> Recording with audio requires the {@link android.Manifest.permission#RECORD_AUDIO}
+ * permission; without it, starting a recording will fail with a {@link SecurityException}.
*
* @param outputOptions the options to store the newly captured video.
+ * @param audioConfig the configuration of audio.
* @param executor the executor that the event listener will be run on.
* @param listener the event listener to handle video record events.
* @return a {@link Recording} that provides controls for new active recordings.
* @throws IllegalStateException if there is an unfinished active recording.
- * @throws SecurityException if the {@link android.Manifest.permission#RECORD_AUDIO}
- * permission is denied.
+ * @throws SecurityException if the audio config specifies audio should be enabled but the
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission is denied.
*/
- @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+ @SuppressLint("MissingPermission")
@ExperimentalVideo
@MainThread
@NonNull
public Recording startRecording(
@NonNull MediaStoreOutputOptions outputOptions,
+ @NonNull AudioConfig audioConfig,
@NonNull Executor executor,
@NonNull Consumer<VideoRecordEvent> listener) {
- return startRecordingInternal(outputOptions, executor, listener);
+ return startRecordingInternal(outputOptions, audioConfig, executor, listener);
}
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@@ -1259,6 +1267,7 @@
@MainThread
private Recording startRecordingInternal(
@NonNull OutputOptions outputOptions,
+ @NonNull AudioConfig audioConfig,
@NonNull Executor executor,
@NonNull Consumer<VideoRecordEvent> listener) {
checkMainThread();
@@ -1268,19 +1277,33 @@
Consumer<VideoRecordEvent> wrappedListener =
wrapListenerToDeactivateRecordingOnFinalized(listener);
- PendingRecording pendingRecording = prepareRecording(outputOptions).withAudioEnabled();
+ PendingRecording pendingRecording = prepareRecording(outputOptions);
+ boolean isAudioEnabled = audioConfig.getAudioEnabled();
+ if (isAudioEnabled) {
+ checkAudioPermissionGranted();
+ pendingRecording.withAudioEnabled();
+ }
Recording recording = pendingRecording.start(executor, wrappedListener);
setActiveRecording(recording, wrappedListener);
return recording;
}
+ private void checkAudioPermissionGranted() {
+ int permissionState = PermissionChecker.checkSelfPermission(mAppContext,
+ Manifest.permission.RECORD_AUDIO);
+ if (permissionState == PermissionChecker.PERMISSION_DENIED) {
+ throw new SecurityException("Attempted to start recording with audio, but "
+ + "application does not have RECORD_AUDIO permission granted.");
+ }
+ }
+
/**
* Generates a {@link PendingRecording} instance for starting a recording.
*
* <p> This method handles {@code prepareRecording()} methods for different output formats,
- * and makes {@link #startRecordingInternal(OutputOptions, Executor, Consumer)} only handle
- * the general flow.
+ * and makes {@link #startRecordingInternal(OutputOptions, AudioConfig, Executor, Consumer)}
+ * only handle the general flow.
*
* <p> This method uses the parent class {@link OutputOptions} as the parameter. On the other
* hand, the public {@code startRecording()} is overloaded with subclasses. The reason is to
diff --git a/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
new file mode 100644
index 0000000..67977b7
--- /dev/null
+++ b/camera/camera-view/src/main/java/androidx/camera/view/video/AudioConfig.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 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.view.video;
+
+import android.Manifest;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RequiresPermission;
+
+/**
+ * A class providing configuration for audio settings in the video recording.
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+@ExperimentalVideo
+public class AudioConfig {
+
+ /**
+ * The audio configuration with audio disabled.
+ */
+ @NonNull
+ public static final AudioConfig AUDIO_DISABLED = new AudioConfig(false);
+
+ private final boolean mIsAudioEnabled;
+
+ AudioConfig(boolean audioEnabled) {
+ mIsAudioEnabled = audioEnabled;
+ }
+
+ /**
+ * Creates a default {@link AudioConfig} with the given audio enabled state.
+ *
+ * <p> The {@link android.Manifest.permission#RECORD_AUDIO} permission is required to
+ * enable audio in video recording; for the use cases where audio is always disabled, please
+ * use {@link AudioConfig#AUDIO_DISABLED} instead, which has no permission requirements.
+ */
+ @RequiresPermission(Manifest.permission.RECORD_AUDIO)
+ @NonNull
+ public static AudioConfig create(boolean enableAudio) {
+ return new AudioConfig(enableAudio);
+ }
+
+ /**
+ * Get the audio enabled state.
+ */
+ public boolean getAudioEnabled() {
+ return mIsAudioEnabled;
+ }
+}
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
index 4ffe7bc..3881ff3 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/CameraControllerFragment.java
@@ -77,6 +77,7 @@
import androidx.camera.view.LifecycleCameraController;
import androidx.camera.view.PreviewView;
import androidx.camera.view.RotationProvider;
+import androidx.camera.view.video.AudioConfig;
import androidx.camera.view.video.ExperimentalVideo;
import androidx.core.util.Consumer;
import androidx.fragment.app.Fragment;
@@ -656,8 +657,9 @@
@OptIn(markerClass = ExperimentalVideo.class)
void startRecording(Consumer<VideoRecordEvent> listener) {
MediaStoreOutputOptions outputOptions = getNewVideoOutputMediaStoreOptions();
- mActiveRecording = mCameraController.startRecording(outputOptions, mExecutorService,
- listener);
+ AudioConfig audioConfig = AudioConfig.create(true);
+ mActiveRecording = mCameraController.startRecording(outputOptions, audioConfig,
+ mExecutorService, listener);
}
@VisibleForTesting