Merge "Fix DeviceCompatibilityTest fail on some devices" into androidx-main
diff --git a/bluetooth/integration-tests/testapp/build.gradle b/bluetooth/integration-tests/testapp/build.gradle
index 6c8a1f3..9856b9f 100644
--- a/bluetooth/integration-tests/testapp/build.gradle
+++ b/bluetooth/integration-tests/testapp/build.gradle
@@ -45,26 +45,25 @@
}
dependencies {
- implementation(libs.kotlinStdlib)
implementation(project(":bluetooth:bluetooth"))
+ implementation(libs.kotlinStdlib)
+ implementation(libs.kotlinCoroutinesAndroid)
+
implementation("androidx.activity:activity-ktx:1.8.0")
- implementation("androidx.appcompat:appcompat:1.6.1")
- implementation(libs.constraintLayout)
implementation("androidx.core:core-ktx:1.12.0")
- implementation("androidx.fragment:fragment-ktx:1.6.1")
+ implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
- implementation("androidx.navigation:navigation-fragment-ktx:2.7.4")
- implementation("androidx.navigation:navigation-ui-ktx:2.7.4")
+ implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
+ implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.recyclerview:recyclerview:1.3.2")
+ implementation(libs.constraintLayout)
+ implementation(libs.material)
+
implementation(libs.hiltAndroid)
kapt(libs.hiltCompiler)
- implementation(libs.material)
-
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
-
kaptAndroidTest(libs.hiltCompiler)
}
diff --git a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
index 45bf89a..a6f68cb 100644
--- a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -9,7 +9,7 @@
android:theme="@style/Theme.TestApp"
tools:ignore="GoogleAppIndexingWarning,MissingApplicationIcon">
<activity
- android:name=".MainActivity"
+ android:name=".ui.main.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index b86258e..5dd5ec4 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -143,8 +143,8 @@
override fun onDestroyView() {
super.onDestroyView()
- _binding = null
isAdvertising = false
+ _binding = null
}
private fun initData() {
@@ -261,7 +261,6 @@
TAG, "bluetoothLe.advertise() called with: " +
"viewModel.advertiseParams = ${viewModel.advertiseParams}"
)
-
isAdvertising = true
bluetoothLe.advertise(viewModel.advertiseParams) {
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/connections/ConnectionsFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/connections/ConnectionsFragment.kt
index ef2b34c..dfef4b3 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/connections/ConnectionsFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/connections/ConnectionsFragment.kt
@@ -13,7 +13,6 @@
import androidx.bluetooth.BluetoothDevice
import androidx.bluetooth.BluetoothLe
import androidx.bluetooth.GattCharacteristic
-import androidx.bluetooth.integration.testapp.MainViewModel
import androidx.bluetooth.integration.testapp.R
import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
import androidx.bluetooth.integration.testapp.data.connection.OnCharacteristicActionClick
@@ -21,6 +20,7 @@
import androidx.bluetooth.integration.testapp.databinding.FragmentConnectionsBinding
import androidx.bluetooth.integration.testapp.ui.common.getColor
import androidx.bluetooth.integration.testapp.ui.common.toast
+import androidx.bluetooth.integration.testapp.ui.main.MainViewModel
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@@ -53,7 +53,7 @@
private var deviceServicesAdapter: DeviceServicesAdapter? = null
- private val connectScope = CoroutineScope(Dispatchers.Default + Job())
+ private val connectScope = CoroutineScope(Dispatchers.Main + Job())
private val onTabSelectedListener = object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: Tab) {
@@ -121,8 +121,8 @@
override fun onDestroyView() {
super.onDestroyView()
- _binding = null
connectScope.cancel()
+ _binding = null
}
private fun initData() {
@@ -185,9 +185,7 @@
deviceConnection.job = connectScope.launch {
deviceConnection.status = Status.CONNECTING
- launch(Dispatchers.Main) {
- updateDeviceUI(deviceConnection)
- }
+ updateDeviceUI(deviceConnection)
try {
Log.d(
@@ -200,9 +198,7 @@
deviceConnection.status = Status.CONNECTED
deviceConnection.services = services
- launch(Dispatchers.Main) {
- updateDeviceUI(deviceConnection)
- }
+ updateDeviceUI(deviceConnection)
deviceConnection.onCharacteristicActionClick =
object : OnCharacteristicActionClick {
@@ -250,9 +246,7 @@
}
deviceConnection.status = Status.DISCONNECTED
- launch(Dispatchers.Main) {
- updateDeviceUI(deviceConnection)
- }
+ updateDeviceUI(deviceConnection)
}
}
}
@@ -269,9 +263,7 @@
Log.d(TAG, "readCharacteristic() result: result = $result")
deviceConnection.storeValueFor(characteristic, result.getOrNull())
- launch(Dispatchers.Main) {
- updateDeviceUI(deviceConnection)
- }
+ updateDeviceUI(deviceConnection)
}
}
@@ -299,9 +291,7 @@
val result = gattClientScope.writeCharacteristic(characteristic, value)
Log.d(TAG, "writeCharacteristic() result: result = $result")
- launch(Dispatchers.Main) {
- toast("Called write with: $editTextValueString, result = $result").show()
- }
+ toast("Called write with: $editTextValueString, result = $result").show()
}
}
.setNegativeButton(getString(R.string.cancel), null)
@@ -339,6 +329,8 @@
@SuppressLint("NotifyDataSetChanged")
private fun updateDeviceUI(deviceConnection: DeviceConnection) {
+ if (_binding == null) return
+
binding.progressIndicatorDeviceConnection.isVisible = false
binding.buttonReconnect.isVisible = false
binding.buttonDisconnect.isVisible = false
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/gatt_server/GattServerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/gatt_server/GattServerFragment.kt
index e778e25..067ee68 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/gatt_server/GattServerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/gatt_server/GattServerFragment.kt
@@ -116,8 +116,8 @@
override fun onDestroyView() {
super.onDestroyView()
- _binding = null
isGattServerOpen = false
+ _binding = null
}
private fun onAddGattService() {
@@ -234,7 +234,6 @@
TAG, "bluetoothLe.openGattServer() called with: " +
"viewModel.gattServerServices = ${viewModel.gattServerServices}"
)
-
isGattServerOpen = true
bluetoothLe.openGattServer(viewModel.gattServerServices) {
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainActivity.kt
similarity index 93%
rename from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
rename to bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainActivity.kt
index e2b26b8..d7c46ac 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainActivity.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.bluetooth.integration.testapp
+package androidx.bluetooth.integration.testapp.ui.main
import android.Manifest
import android.bluetooth.BluetoothAdapter
@@ -28,7 +28,9 @@
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
+import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
+import androidx.bluetooth.integration.testapp.R
import androidx.bluetooth.integration.testapp.databinding.ActivityMainBinding
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
@@ -67,6 +69,8 @@
binding.layoutBluetoothDisabled.isVisible = value.not()
}
+ private val viewModel by viewModels<MainViewModel>()
+
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
@@ -101,6 +105,10 @@
startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE))
}
}
+
+ viewModel.navigateToConnections.observe(this) {
+ binding.bottomNavigationView.selectedItemId = R.id.navigation_connections
+ }
}
override fun onStart() {
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainViewModel.kt
similarity index 68%
rename from bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainViewModel.kt
rename to bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainViewModel.kt
index cf09607..f72c02a 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/main/MainViewModel.kt
@@ -14,10 +14,13 @@
* limitations under the License.
*/
-package androidx.bluetooth.integration.testapp
+package androidx.bluetooth.integration.testapp.ui.main
import androidx.bluetooth.BluetoothDevice
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
+import kotlin.random.Random
class MainViewModel : ViewModel() {
@@ -26,4 +29,12 @@
}
var selectedBluetoothDevice: BluetoothDevice? = null
+
+ val navigateToConnections: LiveData<Int>
+ get() = _navigateToConnections
+ private val _navigateToConnections = MutableLiveData<Int>()
+
+ fun navigateToConnections() {
+ _navigateToConnections.value = Random.nextInt()
+ }
}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
index ecbe691..35b1e8c 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -24,14 +24,13 @@
import android.view.ViewGroup
import androidx.bluetooth.BluetoothDevice
import androidx.bluetooth.BluetoothLe
-import androidx.bluetooth.integration.testapp.MainViewModel
import androidx.bluetooth.integration.testapp.R
import androidx.bluetooth.integration.testapp.databinding.FragmentScannerBinding
import androidx.bluetooth.integration.testapp.ui.common.getColor
+import androidx.bluetooth.integration.testapp.ui.main.MainViewModel
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
-import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import dagger.hilt.android.AndroidEntryPoint
@@ -108,8 +107,8 @@
override fun onDestroyView() {
super.onDestroyView()
- _binding = null
isScanning = false
+ _binding = null
}
@SuppressLint("MissingPermission")
@@ -118,7 +117,6 @@
scanJob = scanScope.launch {
Log.d(TAG, "bluetoothLe.scan() called")
-
isScanning = true
try {
@@ -144,6 +142,6 @@
isScanning = false
mainViewModel.selectedBluetoothDevice = bluetoothDevice
- findNavController().navigate(R.id.action_navigation_scanner_to_navigation_connections)
+ mainViewModel.navigateToConnections()
}
}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 00cafae..46b7f42 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -21,7 +21,7 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- tools:context=".MainActivity">
+ tools:context=".ui.main.MainActivity">
<LinearLayout
android:id="@+id/layout_bluetooth_disabled"
@@ -62,11 +62,11 @@
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
+ app:defaultNavHost="true"
+ app:layout_constraintBottom_toTopOf="@+id/bottom_navigation_view"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/layout_bluetooth_disabled"
- app:layout_constraintBottom_toTopOf="@+id/bottom_navigation_view"
- app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
<com.google.android.material.bottomnavigation.BottomNavigationView
diff --git a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
index 057f265..30cfd4e 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/navigation/nav_graph.xml
@@ -24,20 +24,13 @@
android:id="@+id/navigation_scanner"
android:name="androidx.bluetooth.integration.testapp.ui.scanner.ScannerFragment"
android:label="@string/title_scanner"
- tools:layout="@layout/fragment_scanner" >
- <action
- android:id="@+id/action_navigation_scanner_to_navigation_connections"
- app:destination="@id/navigation_connections"
- app:popUpTo="@id/navigation_scanner"
- app:popUpToInclusive="true" />
- </fragment>
+ tools:layout="@layout/fragment_scanner" />
<fragment
android:id="@+id/navigation_connections"
android:name="androidx.bluetooth.integration.testapp.ui.connections.ConnectionsFragment"
android:label="@string/title_connections"
- tools:layout="@layout/fragment_connections">
- </fragment>
+ tools:layout="@layout/fragment_connections" />
<fragment
android:id="@+id/navigation_advertiser"
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
index 0b02cc5..14bd45f 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/compat/ApiCompat.kt
@@ -33,6 +33,7 @@
import android.hardware.camera2.params.OutputConfiguration
import android.hardware.camera2.params.SessionConfiguration
import android.media.ImageReader
+import android.media.ImageWriter
import android.os.Build
import android.os.Handler
import android.util.Size
@@ -314,6 +315,16 @@
): ImageReader {
return ImageReader.newInstance(width, height, format, capacity, usage)
}
+
+ @JvmStatic
+ @DoNotInline
+ fun imageWriterNewInstance(
+ surface: Surface,
+ maxImages: Int,
+ format: Int
+ ): ImageWriter {
+ return ImageWriter.newInstance(surface, maxImages, format)
+ }
}
@RequiresApi(Build.VERSION_CODES.R)
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageReaders.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageReaders.kt
index f7bfb6f..8da15df 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageReaders.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageReaders.kt
@@ -102,7 +102,7 @@
// One of the worst cases observed is the HAL reserving 10 images, which gives a maximum
// capacity of 54 (64 - 10). For safety and compatibility reasons, set the maximum capacity
// to be 54, which leaves headroom for an app configured limit of 50.
- internal const val IMAGERREADER_MAX_CAPACITY = 54
+ internal const val IMAGEREADER_MAX_CAPACITY = 54
/**
* Create and configure a new ImageReader instance as an [ImageReaderWrapper].
@@ -121,9 +121,9 @@
require(width > 0) { "Width ($width) must be > 0" }
require(height > 0) { "Height ($height) must be > 0" }
require(capacity > 0) { "Capacity ($capacity) must be > 0" }
- require(capacity <= IMAGERREADER_MAX_CAPACITY) {
+ require(capacity <= IMAGEREADER_MAX_CAPACITY) {
"Capacity for creating new ImageSources is restricted to " +
- "$IMAGERREADER_MAX_CAPACITY. Android has undocumented internal limits that " +
+ "$IMAGEREADER_MAX_CAPACITY. Android has undocumented internal limits that " +
"are different depending on which device the ImageReader is created on."
}
@@ -231,9 +231,9 @@
executor: Executor
): ImageReaderWrapper {
require(capacity > 0) { "Capacity ($capacity) must be > 0" }
- require(capacity <= AndroidImageReader.IMAGERREADER_MAX_CAPACITY) {
+ require(capacity <= AndroidImageReader.IMAGEREADER_MAX_CAPACITY) {
"Capacity for creating new ImageSources is restricted to " +
- "${AndroidImageReader.IMAGERREADER_MAX_CAPACITY}. Android has undocumented " +
+ "${AndroidImageReader.IMAGEREADER_MAX_CAPACITY}. Android has undocumented " +
"internal limits that are different depending on which device the " +
"MultiResolutionImageReader is created on."
}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
new file mode 100644
index 0000000..28ec921
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/AndroidImageWriter.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.camera2.pipe.media
+
+import android.media.Image
+import android.media.ImageWriter
+import android.os.Build
+import android.os.Handler
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.InputId
+import androidx.camera.camera2.pipe.StreamFormat
+import androidx.camera.camera2.pipe.compat.Api29Compat
+import androidx.camera.camera2.pipe.core.Log
+import androidx.camera.camera2.pipe.media.AndroidImageReader.Companion.IMAGEREADER_MAX_CAPACITY
+import kotlin.reflect.KClass
+import kotlinx.atomicfu.atomic
+
+/**
+ * Implements an [ImageWriterWrapper] using an [ImageWriter].
+ */
+@RequiresApi(Build.VERSION_CODES.M)
+class AndroidImageWriter private constructor(
+ private val imageWriter: ImageWriter,
+ private val inputId: InputId
+) : ImageWriterWrapper, ImageWriter.OnImageReleasedListener {
+ private val onImageReleasedListener = atomic<ImageWriterWrapper.OnImageReleasedListener?>(null)
+ override val maxImages: Int = imageWriter.maxImages
+
+ override val format: Int = imageWriter.format
+
+ override fun queueInputImage(image: ImageWrapper) {
+ imageWriter.queueInputImage(image.unwrapAs(Image::class))
+ }
+
+ override fun dequeueInputImage(): ImageWrapper {
+ val image = imageWriter.dequeueInputImage()
+ return AndroidImage(image)
+ }
+
+ override fun setOnImageReleasedListener(
+ onImageReleasedListener: ImageWriterWrapper.OnImageReleasedListener
+ ) {
+ this.onImageReleasedListener.value = onImageReleasedListener
+ }
+
+ override fun onImageReleased(writer: ImageWriter?) {
+ onImageReleasedListener.value?.onImageReleased(inputId)
+ }
+
+ override fun close() = imageWriter.close()
+
+ @Suppress("UNCHECKED_CAST")
+ override fun <T : Any> unwrapAs(type: KClass<T>): T? = when (type) {
+ ImageWriter::class -> imageWriter as T?
+ else -> null
+ }
+
+ override fun toString(): String {
+ return "ImageWriter-${StreamFormat(imageWriter.format).name}-" +
+ "inputId$inputId"
+ }
+
+ companion object {
+ /**
+ * Create and configure a new ImageWriter instance as an [ImageWriter].
+ *
+ * See [ImageWriter.newInstance] for details.
+ */
+ fun create(
+ surface: Surface,
+ maxImages: Int,
+ format: Int?,
+ inputId: InputId,
+ handler: Handler
+ ): ImageWriterWrapper {
+ require(maxImages > 0) { "Max images ($maxImages) must be > 0" }
+ require(maxImages <= IMAGEREADER_MAX_CAPACITY) {
+ "Max images for ImageWriters is restricted to " +
+ "$IMAGEREADER_MAX_CAPACITY to prevent overloading downstream " +
+ "consumer components."
+ }
+
+ // Create and configure a new ImageWriter
+ val imageWriter =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && format != null) {
+ Api29Compat.imageWriterNewInstance(surface, maxImages, format)
+ } else {
+ if (format != null) {
+ Log.warn {
+ "Ignoring format ($format) for $inputId. Android " +
+ "${Build.VERSION.SDK_INT} does not support creating ImageWriters " +
+ "with formats. This may lead to unexpected behaviors."
+ }
+ }
+ ImageWriter.newInstance(surface, maxImages)
+ }
+
+ val androidImageWriter = AndroidImageWriter(imageWriter, inputId)
+ imageWriter.setOnImageReleasedListener(
+ androidImageWriter, handler
+ )
+ return androidImageWriter
+ }
+ }
+}
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageSource.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageSource.kt
index 89a7b4e..2efd693 100644
--- a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageSource.kt
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageSource.kt
@@ -26,7 +26,7 @@
import androidx.camera.camera2.pipe.OutputId
import androidx.camera.camera2.pipe.UnsafeWrapper
import androidx.camera.camera2.pipe.core.Log
-import androidx.camera.camera2.pipe.media.AndroidImageReader.Companion.IMAGERREADER_MAX_CAPACITY
+import androidx.camera.camera2.pipe.media.AndroidImageReader.Companion.IMAGEREADER_MAX_CAPACITY
import java.util.concurrent.Executor
import kotlin.reflect.KClass
import kotlinx.atomicfu.atomic
@@ -67,7 +67,7 @@
companion object {
private const val IMAGE_CAPACITY_MARGIN = 2
- private const val IMAGE_SOURCE_CAPACITY = IMAGERREADER_MAX_CAPACITY - IMAGE_CAPACITY_MARGIN
+ private const val IMAGE_SOURCE_CAPACITY = IMAGEREADER_MAX_CAPACITY - IMAGE_CAPACITY_MARGIN
fun create(
imageReader: ImageReaderWrapper
diff --git a/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
new file mode 100644
index 0000000..57b4d9ee
--- /dev/null
+++ b/camera/camera-camera2-pipe/src/main/java/androidx/camera/camera2/pipe/media/ImageWriterWrapper.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.camera2.pipe.media
+
+import android.media.ImageWriter
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.camera.camera2.pipe.InputId
+import androidx.camera.camera2.pipe.UnsafeWrapper
+
+/**
+ * Simplified wrapper for [ImageWriter]-like classes.
+ */
+
+@RequiresApi(Build.VERSION_CODES.M)
+interface ImageWriterWrapper : UnsafeWrapper, AutoCloseable {
+
+ /**
+ * Get the ImageWriter format.
+ * @see [ImageWriter.getFormat]
+ */
+ val format: Int
+
+ /**
+ * Get the maximum number of images that can be dequeued from the ImageWriter simultaneously.
+ * @see [ImageWriter.getMaxImages]
+ */
+ val maxImages: Int
+
+ /**
+ * Queue an input Image back to ImageWriter for the downstream consumer to access.
+ * @see [ImageWriter.queueInputImage]
+ */
+ fun queueInputImage(image: ImageWrapper)
+
+ /**
+ * Dequeue the next available input Image for the application to produce data into.
+ * @see [ImageWriter.dequeueInputImage]
+ */
+ fun dequeueInputImage(): ImageWrapper
+
+ /**
+ * Set the [OnImageReleasedListener]. Setting additional listeners will override the previous listener.]
+ */
+ fun setOnImageReleasedListener(onImageReleasedListener: OnImageReleasedListener)
+
+ /**
+ * The OnImageListener adapts the standard [ImageWriter.OnImageReleasedListener] to retrieve
+ * images returned to the ImageWriter.
+ */
+ fun interface OnImageReleasedListener {
+ /**
+ * Handle the [ImageWrapper] that has been released back to [ImageWriterWrapper].
+ */
+ fun onImageReleased(inputId: InputId)
+ }
+
+ interface Builder {
+ fun build(): ImageWriterWrapper
+ }
+}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
index 6537491..84c5c81 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraControlImpl.java
@@ -661,11 +661,14 @@
mZoomControl.addZoomOption(builder);
int aeMode = CaptureRequest.CONTROL_AE_MODE_ON;
+
+ if (mFocusMeteringControl.isExternalFlashAeModeEnabled()) {
+ aeMode = CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH;
+ }
+
if (mIsTorchOn) {
builder.setCaptureRequestOptionWithPriority(CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH, Config.OptionPriority.REQUIRED);
- } else if (mFocusMeteringControl.isExternalFlashAeModeEnabled()) {
- aeMode = CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH;
} else {
switch (mFlashMode) {
case FLASH_MODE_OFF:
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
index 247473c..2125a4b 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CapturePipeline.java
@@ -733,6 +733,17 @@
uiAppliedFuture),
mExecutor
).transformAsync(
+ // Won't have any effect if CONTROL_AE_MODE_ON_EXTERNAL_FLASH is supported
+ input -> CallbackToFutureAdapter.getFuture(
+ completer -> {
+ Logger.d(TAG, "ScreenFlashTask#preCapture: enable torch");
+ // TODO: Enable torch only if actual flash unit doesn't exist
+ mCameraControl.enableTorchInternal(true);
+ completer.set(null);
+ return "EnableTorchInternal";
+ }),
+ mExecutor
+ ).transformAsync(
input -> mCameraControl.getFocusMeteringControl().triggerAePrecapture(),
mExecutor
).transformAsync(
@@ -751,6 +762,7 @@
@Override
public void postCapture() {
Logger.d(TAG, "ScreenFlashTask#postCapture");
+ mCameraControl.enableTorchInternal(false);
mCameraControl.getFocusMeteringControl().enableExternalFlashAeMode(false).addListener(
() -> Log.d(TAG, "enableExternalFlashAeMode disabled"), mExecutor
);