Merge "Version bump to alpha04" into androidx-main
diff --git a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
index 74a5c35..6f91eca 100644
--- a/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
+++ b/camera/integration-tests/avsynctestapp/src/androidTest/java/androidx/camera/integration/avsync/model/AudioGeneratorDeviceTest.kt
@@ -16,7 +16,9 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioTrack
+import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
@@ -33,6 +35,7 @@
@RunWith(AndroidJUnit4::class)
class AudioGeneratorDeviceTest {
+ private val context: Context = ApplicationProvider.getApplicationContext()
private lateinit var audioGenerator: AudioGenerator
@Before
@@ -42,12 +45,12 @@
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenFrequencyNegative() = runTest {
- audioGenerator.initAudioTrack(-5300, 11.0)
+ audioGenerator.initAudioTrack(context, -5300, 11.0)
}
@Test(expected = IllegalArgumentException::class)
fun initAudioTrack_throwExceptionWhenLengthNegative() = runTest {
- audioGenerator.initAudioTrack(5300, -11.0)
+ audioGenerator.initAudioTrack(context, 5300, -11.0)
}
@Test
@@ -71,7 +74,7 @@
}
private suspend fun initialAudioTrack(frequency: Int, beepLengthInSec: Double) {
- val isInitialized = audioGenerator.initAudioTrack(frequency, beepLengthInSec)
+ val isInitialized = audioGenerator.initAudioTrack(context, frequency, beepLengthInSec)
assertThat(isInitialized).isTrue()
assertThat(audioGenerator.audioTrack!!.state).isEqualTo(AudioTrack.STATE_INITIALIZED)
assertThat(audioGenerator.audioTrack!!.playbackHeadPosition).isEqualTo(0)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
index 2997a17..2762f2d 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/SignalGeneratorViewModel.kt
@@ -79,6 +79,7 @@
withContext(Dispatchers.Default) {
audioGenerator.initAudioTrack(
+ context = context,
frequency = beepFrequency,
beepLengthInSec = ACTIVE_LENGTH_SEC,
)
diff --git a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
index 80c3936..8e12eb2 100644
--- a/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
+++ b/camera/integration-tests/avsynctestapp/src/main/java/androidx/camera/integration/avsync/model/AudioGenerator.kt
@@ -16,6 +16,7 @@
package androidx.camera.integration.avsync.model
+import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFormat
import android.media.AudioManager
@@ -30,8 +31,8 @@
import kotlin.math.sin
private const val TAG = "AudioGenerator"
+private const val DEFAULT_SAMPLE_RATE: Int = 44100
private const val SAMPLE_WIDTH: Int = 2
-private const val SAMPLE_RATE: Int = 44100
private const val MAGNITUDE = 0.5
private const val ENCODING: Int = AudioFormat.ENCODING_PCM_16BIT
private const val CHANNEL = AudioFormat.CHANNEL_OUT_MONO
@@ -46,26 +47,33 @@
}
fun stop() {
+ Logger.i(TAG, "playState before stopped: ${audioTrack!!.playState}")
+ Logger.i(TAG, "playbackHeadPosition before stopped: ${audioTrack!!.playbackHeadPosition}")
audioTrack!!.stop()
}
suspend fun initAudioTrack(
+ context: Context,
frequency: Int,
beepLengthInSec: Double,
): Boolean {
checkArgumentNonnegative(frequency, "The input frequency should not be negative.")
checkArgument(beepLengthInSec >= 0, "The beep length should not be negative.")
- Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
-
- val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, SAMPLE_RATE)
+ val sampleRate = getOutputSampleRate(context)
+ val samples = generateSineSamples(frequency, beepLengthInSec, SAMPLE_WIDTH, sampleRate)
val bufferSize = samples.size
+
+ Logger.i(TAG, "initAudioTrack with sample rate: $sampleRate")
+ Logger.i(TAG, "initAudioTrack with beep frequency: $frequency")
+ Logger.i(TAG, "initAudioTrack with buffer size: $bufferSize")
+
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val audioFormat = AudioFormat.Builder()
- .setSampleRate(SAMPLE_RATE)
+ .setSampleRate(sampleRate)
.setEncoding(ENCODING)
.setChannelMask(CHANNEL)
.build()
@@ -83,6 +91,13 @@
return true
}
+ private fun getOutputSampleRate(context: Context): Int {
+ val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ val sampleRate: String? = audioManager.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE)
+
+ return sampleRate?.toInt() ?: DEFAULT_SAMPLE_RATE
+ }
+
@VisibleForTesting
suspend fun generateSineSamples(
frequency: Int,
diff --git a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
index 0636c9b..981b5d2 100644
--- a/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
+++ b/camera/integration-tests/coretestapp/src/androidTest/java/androidx/camera/integration/core/ExistingActivityLifecycleTest.kt
@@ -18,10 +18,13 @@
import android.Manifest
import android.app.Instrumentation
import android.content.Context
+import android.content.Intent
import android.os.Build
import androidx.camera.camera2.Camera2Config
+import androidx.camera.camera2.pipe.integration.CameraPipeConfig
import androidx.camera.core.CameraSelector
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
@@ -29,10 +32,9 @@
import androidx.lifecycle.Lifecycle.State.RESUMED
import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
-import androidx.test.espresso.Espresso
+import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
-import androidx.test.espresso.matcher.ViewMatchers
-import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
@@ -42,20 +44,23 @@
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.runBlocking
import org.junit.After
-import org.junit.AfterClass
import org.junit.Assume
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
private const val HOME_TIMEOUT_MS = 3000L
private const val ROTATE_TIMEOUT_MS = 2000L
// Test application lifecycle when using CameraX.
-@RunWith(AndroidJUnit4::class)
+@RunWith(Parameterized::class)
@LargeTest
-class ExistingActivityLifecycleTest {
+class ExistingActivityLifecycleTest(
+ private val implName: String,
+ private val cameraConfig: String
+) {
private val device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@get:Rule
@@ -73,14 +78,17 @@
@get:Rule
val repeatRule = RepeatRule()
- companion object {
- @AfterClass
- @JvmStatic
- fun shutdownCameraX() {
- val context = ApplicationProvider.getApplicationContext<Context>()
- val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
- cameraProvider.shutdown()[10, TimeUnit.SECONDS]
- }
+ @get:Rule
+ val cameraPipeConfigTestRule = CameraPipeConfigTestRule(
+ active = implName == CameraPipeConfig::class.simpleName,
+ forAllTests = true,
+ )
+
+ private val launchIntent = Intent(
+ ApplicationProvider.getApplicationContext(),
+ CameraXActivity::class.java
+ ).apply {
+ putExtra(CameraXActivity.INTENT_EXTRA_CAMERA_IMPLEMENTATION, cameraConfig)
}
@Before
@@ -108,13 +116,17 @@
device.unfreezeRotation()
device.pressHome()
device.waitForIdle(HOME_TIMEOUT_MS)
+
+ val context = ApplicationProvider.getApplicationContext<Context>()
+ val cameraProvider = ProcessCameraProvider.getInstance(context)[10, TimeUnit.SECONDS]
+ cameraProvider.shutdown()[10, TimeUnit.SECONDS]
}
// Check if Preview screen is updated or not, after Destroy-Create lifecycle.
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -129,7 +141,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterDestroyRecreate() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -150,7 +162,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -179,7 +191,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -210,13 +222,13 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
// Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Check front camera is now idle
@@ -244,7 +256,7 @@
)
)
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -252,7 +264,7 @@
waitForViewfinderIdle()
// Act. Switch camera.
- Espresso.onView(ViewMatchers.withId(R.id.direction_toggle))
+ onView(withId(R.id.direction_toggle))
.perform(ViewActions.click())
// Assert.
@@ -272,7 +284,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkPreviewUpdatedAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use { // Ensure ActivityScenario is cleaned up properly
// Wait for viewfinder to receive enough frames for its IdlingResource to idle.
waitForViewfinderIdle()
@@ -298,7 +310,7 @@
@Test
@RepeatRule.Repeat(times = 5)
fun checkImageCaptureAfterRotateDeviceAndStopResume() {
- with(ActivityScenario.launch(CameraXActivity::class.java)) { // Launch activity.
+ with(ActivityScenario.launch<CameraXActivity>(launchIntent)) { // Launch activity.
use {
// Arrange.
// Ensure ActivityScenario is cleaned up properly
@@ -337,4 +349,20 @@
)
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
}
+
+ companion object {
+
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun data() = listOf(
+ arrayOf(
+ Camera2Config::class.simpleName,
+ CameraXViewModel.CAMERA2_IMPLEMENTATION_OPTION
+ ),
+ arrayOf(
+ CameraPipeConfig::class.simpleName,
+ CameraXViewModel.CAMERA_PIPE_IMPLEMENTATION_OPTION
+ )
+ )
+ }
}
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
index 38b9612..0eb3827 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/Diagnosis.kt
@@ -19,7 +19,6 @@
import android.content.Context
import android.os.Build
import android.util.Log
-import android.widget.Toast
import java.io.File
import java.io.FileOutputStream
import java.util.zip.ZipEntry
@@ -30,8 +29,8 @@
*/
class Diagnosis {
- // TODO: convert to async function
- fun collectDeviceInfo(context: Context) {
+ // TODO: convert to a suspend function for running different tasks within this function
+ fun collectDeviceInfo(context: Context): File {
Log.d(TAG, "calling collectDeviceInfo()")
// TODO: verify if external storage is available
@@ -57,12 +56,7 @@
zout.close()
fout.close()
- Log.d(TAG, "file at ${tempFile.path}")
- if (tempFile.exists()) {
- val msg = "Successfully collected information"
- Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
- Log.d(TAG, msg)
- }
+ return tempFile
}
private fun createTemp(context: Context, filename: String): File {
diff --git a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
index 519dad6..f195557 100644
--- a/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
+++ b/camera/integration-tests/diagnosetestapp/src/main/java/androidx/camera/integration/diagnose/MainActivity.kt
@@ -32,7 +32,6 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
-import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.view.CameraController
import androidx.camera.view.CameraController.IMAGE_CAPTURE
import androidx.camera.view.CameraController.VIDEO_CAPTURE
@@ -46,13 +45,21 @@
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Locale
-import java.util.concurrent.Executor
+import java.util.concurrent.ExecutorService
import androidx.camera.mlkit.vision.MlKitAnalyzer
import androidx.camera.view.CameraController.IMAGE_ANALYSIS
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
import com.google.android.material.tabs.TabLayout
import com.google.mlkit.vision.barcode.BarcodeScanner
import com.google.mlkit.vision.barcode.BarcodeScanning
import java.io.IOException
+import java.util.concurrent.Executor
+import java.util.concurrent.Executors
+import kotlinx.coroutines.ExecutorCoroutineDispatcher
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
class MainActivity : AppCompatActivity() {
@@ -65,6 +72,9 @@
private lateinit var barcodeScanner: BarcodeScanner
private lateinit var analyzer: MlKitAnalyzer
private lateinit var diagnoseBtn: Button
+ private lateinit var calibrationExecutor: ExecutorService
+ private var calibrationThreadId: Long = -1
+ private lateinit var diagnosisDispatcher: ExecutorCoroutineDispatcher
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,6 +89,13 @@
diagnosis = Diagnosis()
barcodeScanner = BarcodeScanning.getClient()
diagnoseBtn = findViewById(R.id.diagnose_btn)
+ calibrationExecutor = Executors.newSingleThreadExecutor() { runnable ->
+ val thread = Executors.defaultThreadFactory().newThread(runnable)
+ thread.name = "CalibrationThread"
+ calibrationThreadId = thread.id
+ return@newSingleThreadExecutor thread
+ }
+ diagnosisDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
// Request CAMERA permission and fail gracefully if not granted.
if (allPermissionsGranted()) {
@@ -125,12 +142,20 @@
})
diagnoseBtn.setOnClickListener {
- try {
- diagnosis.collectDeviceInfo(baseContext)
- } catch (e: IOException) {
- val msg = "Failed to collect information"
- Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
- Log.e(TAG, "IOException caught: ${e.message}")
+ lifecycleScope.launch {
+ try {
+ val tempFile = withContext(diagnosisDispatcher) {
+ Log.i(TAG, "dispatcher: ${Thread.currentThread().name}")
+ diagnosis.collectDeviceInfo(baseContext)
+ }
+ Log.d(TAG, "file at ${tempFile.path}")
+ val msg = "Successfully collected device info"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ } catch (e: IOException) {
+ val msg = "Failed to collect information"
+ Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
+ Log.e(TAG, "IOException caught: ${e.message}")
+ }
}
}
}
@@ -287,20 +312,25 @@
analyzer = MlKitAnalyzer(
listOf(barcodeScanner),
CameraController.COORDINATE_SYSTEM_VIEW_REFERENCED,
- CameraXExecutors.mainThreadExecutor()
+ calibrationExecutor
) { result ->
+ // validating thread
+ checkCalibrationThread()
val barcodes = result.getValue(barcodeScanner)
if (barcodes != null && barcodes.size > 0) {
calibrate.analyze(barcodes)
- // gives overlayView access to Calibration
- overlayView.setCalibrationResult(calibrate)
- // enable diagnose button when alignment is successful
- diagnoseBtn.isEnabled = calibrate.isAligned
- overlayView.invalidate()
+ // run UI on main thread
+ lifecycleScope.launch {
+ // gives overlayView access to Calibration
+ overlayView.setCalibrationResult(calibrate)
+ // enable diagnose button when alignment is successful
+ diagnoseBtn.isEnabled = calibrate.isAligned
+ overlayView.invalidate()
+ }
}
}
cameraController.setImageAnalysisAnalyzer(
- CameraXExecutors.mainThreadExecutor(), analyzer)
+ calibrationExecutor, analyzer)
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
@@ -309,6 +339,17 @@
) == PackageManager.PERMISSION_GRANTED
}
+ private fun checkCalibrationThread() {
+ Preconditions.checkState(calibrationThreadId == Thread.currentThread().id,
+ "Not working on Calibration Thread")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ calibrationExecutor.shutdown()
+ diagnosisDispatcher.close()
+ }
+
companion object {
private const val TAG = "DiagnoseApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 5aa38fc..48ca50a 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
<activity
android:name=".CameraExtensionsActivity"
android:exported="true"
- android:label="Camera Extensions">
+ android:label="CameraX Extensions">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -35,6 +35,16 @@
</activity>
<activity
+ android:name=".Camera2ExtensionsActivity"
+ android:exported="false"
+ android:label="Camera2 Extensions">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+
+ <activity
android:name=".validation.CameraValidationResultActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:exported="false">
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
new file mode 100644
index 0000000..d682e82
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/Camera2ExtensionsActivity.kt
@@ -0,0 +1,661 @@
+/*
+ * 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.integration.extensions
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.params.ExtensionSessionConfiguration
+import android.hardware.camera2.params.OutputConfiguration
+import android.media.ImageReader
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import android.view.Menu
+import android.view.MenuItem
+import android.view.Surface
+import android.view.TextureView
+import android.view.ViewStub
+import android.widget.Button
+import android.widget.Toast
+import androidx.annotation.RequiresApi
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.impl.utils.futures.Futures
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.calculateRelativeImageRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.createExtensionCaptureCallback
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getDisplayRotationDegrees
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getLensFacingCameraId
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickPreviewResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.pickStillImageResolution
+import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.transformPreview
+import androidx.camera.integration.extensions.utils.FileUtil
+import androidx.camera.integration.extensions.validation.CameraValidationResultActivity
+import androidx.concurrent.futures.CallbackToFutureAdapter
+import androidx.concurrent.futures.CallbackToFutureAdapter.Completer
+import androidx.core.util.Preconditions
+import androidx.lifecycle.lifecycleScope
+import com.google.common.util.concurrent.ListenableFuture
+import java.text.Format
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Locale
+import java.util.concurrent.Executors
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import kotlinx.coroutines.asExecutor
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+private const val TAG = "Camera2ExtensionsAct~"
+private const val EXTENSION_MODE_INVALID = -1
+
+@RequiresApi(31)
+class Camera2ExtensionsActivity : AppCompatActivity() {
+
+ private lateinit var cameraManager: CameraManager
+
+ /**
+ * A reference to the opened [CameraDevice].
+ */
+ private var cameraDevice: CameraDevice? = null
+
+ /**
+ * The current camera extension session.
+ */
+ private var cameraExtensionSession: CameraExtensionSession? = null
+
+ private var currentCameraId = "0"
+
+ private lateinit var backCameraId: String
+ private lateinit var frontCameraId: String
+
+ private var cameraSensorRotationDegrees = 0
+
+ /**
+ * Still capture image reader
+ */
+ private var stillImageReader: ImageReader? = null
+
+ /**
+ * Camera extension characteristics for the current camera device.
+ */
+ private lateinit var extensionCharacteristics: CameraExtensionCharacteristics
+
+ /**
+ * Flag whether we should restart preview after an extension switch.
+ */
+ private var restartPreview = false
+
+ /**
+ * Flag whether we should restart after an camera switch.
+ */
+ private var restartCamera = false
+
+ /**
+ * Track current extension type and index.
+ */
+ private var currentExtensionMode = EXTENSION_MODE_INVALID
+ private var currentExtensionIdx = -1
+ private val supportedExtensionModes = mutableListOf<Int>()
+
+ private lateinit var textureView: TextureView
+
+ private lateinit var previewSurface: Surface
+
+ private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
+
+ override fun onSurfaceTextureAvailable(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ previewSurface = Surface(surfaceTexture)
+ openCameraWithExtensionMode(currentCameraId)
+ }
+
+ override fun onSurfaceTextureSizeChanged(
+ surfaceTexture: SurfaceTexture,
+ with: Int,
+ height: Int
+ ) {
+ }
+
+ override fun onSurfaceTextureDestroyed(surfaceTexture: SurfaceTexture): Boolean {
+ return true
+ }
+
+ override fun onSurfaceTextureUpdated(surfaceTexture: SurfaceTexture) {
+ }
+ }
+
+ private val captureCallbacks = createExtensionCaptureCallback()
+
+ private var restartOnStart = false
+
+ private var activityStopped = false
+
+ private val cameraTaskDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
+
+ private var imageSaveTerminationFuture: ListenableFuture<Any?> = Futures.immediateFuture(null)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "onCreate()")
+ setContentView(R.layout.activity_camera_extensions)
+
+ cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
+ backCameraId = getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_BACK)
+ frontCameraId =
+ getLensFacingCameraId(cameraManager, CameraCharacteristics.LENS_FACING_FRONT)
+
+ currentCameraId = if (isCameraSupportExtensions(backCameraId)) {
+ backCameraId
+ } else if (isCameraSupportExtensions(frontCameraId)) {
+ frontCameraId
+ } else {
+ Toast.makeText(
+ this,
+ "Can't find camera supporting Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return
+ }
+
+ updateExtensionInfo()
+
+ setupTextureView()
+ enableUiControl(false)
+ setupUiControl()
+ }
+
+ private fun isCameraSupportExtensions(cameraId: String): Boolean {
+ val characteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ return characteristics.supportedExtensions.isNotEmpty()
+ }
+
+ private fun updateExtensionInfo() {
+ Log.d(
+ TAG,
+ "updateExtensionInfo() - camera Id: $currentCameraId, ${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+ )
+ extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(currentCameraId)
+ supportedExtensionModes.clear()
+ supportedExtensionModes.addAll(extensionCharacteristics.supportedExtensions)
+
+ cameraSensorRotationDegrees = cameraManager.getCameraCharacteristics(
+ currentCameraId)[CameraCharacteristics.SENSOR_ORIENTATION] ?: 0
+
+ currentExtensionIdx = -1
+
+ // Checks whether the original selected extension mode is supported by the new target camera
+ if (currentExtensionMode != EXTENSION_MODE_INVALID) {
+ for (i in 0..supportedExtensionModes.size) {
+ if (supportedExtensionModes[i] == currentExtensionMode) {
+ currentExtensionIdx = i
+ break
+ }
+ }
+ }
+
+ // Switches to the first supported extension mode if the original selected mode is not
+ // supported
+ if (currentExtensionIdx == -1) {
+ currentExtensionIdx = 0
+ currentExtensionMode = supportedExtensionModes[0]
+ }
+ }
+
+ private fun setupTextureView() {
+ val viewFinderStub = findViewById<ViewStub>(R.id.viewFinderStub)
+ viewFinderStub.layoutResource = R.layout.full_textureview
+ textureView = viewFinderStub.inflate() as TextureView
+ textureView.surfaceTextureListener = surfaceTextureListener
+ }
+
+ private fun enableUiControl(enabled: Boolean) {
+ findViewById<Button>(R.id.PhotoToggle).isEnabled = enabled
+ findViewById<Button>(R.id.Switch).isEnabled = enabled
+ findViewById<Button>(R.id.Picture).isEnabled = enabled
+ }
+
+ private fun setupUiControl() {
+ val extensionModeToggleButton = findViewById<Button>(R.id.PhotoToggle)
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+ extensionModeToggleButton.setOnClickListener {
+ enableUiControl(false)
+ currentExtensionIdx = (currentExtensionIdx + 1) % supportedExtensionModes.size
+ currentExtensionMode = supportedExtensionModes[currentExtensionIdx]
+ restartPreview = true
+ extensionModeToggleButton.text = getExtensionModeStringFromId(currentExtensionMode)
+
+ closeCaptureSession()
+ }
+
+ val cameraSwitchButton = findViewById<Button>(R.id.Switch)
+ cameraSwitchButton.setOnClickListener {
+ val newCameraId = if (currentCameraId == backCameraId) frontCameraId else backCameraId
+
+ if (!isCameraSupportExtensions(newCameraId)) {
+ Toast.makeText(
+ this,
+ "Camera of the other lens facing doesn't support Camera2 extensions.",
+ Toast.LENGTH_SHORT
+ ).show()
+ return@setOnClickListener
+ }
+
+ enableUiControl(false)
+ currentCameraId = newCameraId
+ restartCamera = true
+
+ closeCamera()
+ }
+
+ val captureButton = findViewById<Button>(R.id.Picture)
+ captureButton.setOnClickListener {
+ enableUiControl(false)
+ takePicture()
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ Log.d(TAG, "onStart()")
+ activityStopped = false
+ if (restartOnStart) {
+ restartOnStart = false
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+
+ override fun onStop() {
+ Log.d(TAG, "onStop()++")
+ super.onStop()
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+ restartOnStart = true
+ activityStopped = true
+ Log.d(TAG, "onStop()--")
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "onDestroy()++")
+ super.onDestroy()
+ previewSurface.release()
+
+ imageSaveTerminationFuture.addListener({ stillImageReader?.close() }, mainExecutor)
+ Log.d(TAG, "onDestroy()--")
+ }
+
+ private fun closeCamera(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCamera()++")
+ cameraDevice?.close()
+ cameraDevice = null
+ Log.d(TAG, "closeCamera()--")
+ }
+
+ private fun closeCaptureSession(): Deferred<Unit> = lifecycleScope.async(cameraTaskDispatcher) {
+ Log.d(TAG, "closeCaptureSession()++")
+ try {
+ cameraExtensionSession?.close()
+ cameraExtensionSession = null
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ Log.d(TAG, "closeCaptureSession()--")
+ }
+
+ private fun openCameraWithExtensionMode(cameraId: String) =
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ Log.d(TAG, "openCameraWithExtensionMode()++ cameraId: $cameraId")
+ cameraDevice = openCamera(cameraManager, cameraId)
+ cameraExtensionSession = openCaptureSession()
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (activityStopped) {
+ closeCaptureSession()
+ closeCamera()
+ }
+ }
+ Log.d(TAG, "openCameraWithExtensionMode()--")
+ }
+
+ /**
+ * Opens and returns the camera (as the result of the suspend coroutine)
+ */
+ @SuppressLint("MissingPermission")
+ suspend fun openCamera(
+ manager: CameraManager,
+ cameraId: String,
+ ): CameraDevice = suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCamera(): $cameraId")
+ manager.openCamera(
+ cameraId,
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraDevice.StateCallback() {
+ override fun onOpened(device: CameraDevice) = cont.resume(device)
+
+ override fun onDisconnected(device: CameraDevice) {
+ Log.w(TAG, "Camera $cameraId has been disconnected")
+ finish()
+ }
+
+ override fun onClosed(camera: CameraDevice) {
+ Log.d(TAG, "Camera - onClosed: $cameraId")
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartCamera) {
+ restartCamera = false
+ updateExtensionInfo()
+ openCameraWithExtensionMode(currentCameraId)
+ }
+ }
+ }
+
+ override fun onError(device: CameraDevice, error: Int) {
+ Log.d(TAG, "Camera - onError: $cameraId")
+ val msg = when (error) {
+ ERROR_CAMERA_DEVICE -> "Fatal (device)"
+ ERROR_CAMERA_DISABLED -> "Device policy"
+ ERROR_CAMERA_IN_USE -> "Camera in use"
+ ERROR_CAMERA_SERVICE -> "Fatal (service)"
+ ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+ else -> "Unknown"
+ }
+ val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
+ Log.e(TAG, exc.message, exc)
+ cont.resumeWithException(exc)
+ }
+ })
+ }
+
+ /**
+ * Opens and returns the extensions session (as the result of the suspend coroutine)
+ */
+ private suspend fun openCaptureSession(): CameraExtensionSession =
+ suspendCancellableCoroutine { cont ->
+ Log.d(TAG, "openCaptureSession")
+ setupPreview()
+
+ if (stillImageReader != null) {
+ val imageReaderToClose = stillImageReader!!
+ imageSaveTerminationFuture.addListener(
+ { imageReaderToClose.close() },
+ mainExecutor
+ )
+ }
+
+ stillImageReader = setupImageReader()
+
+ val outputConfig = ArrayList<OutputConfiguration>()
+ outputConfig.add(OutputConfiguration(stillImageReader!!.surface))
+ outputConfig.add(OutputConfiguration(previewSurface))
+ val extensionConfiguration = ExtensionSessionConfiguration(
+ currentExtensionMode, outputConfig,
+ cameraTaskDispatcher.asExecutor(), object : CameraExtensionSession.StateCallback() {
+ override fun onClosed(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onClosed: $session")
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ if (restartPreview) {
+ restartPreview = false
+
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ cameraExtensionSession = openCaptureSession()
+ }
+ }
+ }
+ }
+
+ override fun onConfigured(session: CameraExtensionSession) {
+ Log.d(TAG, "CaptureSession - onConfigured: $session")
+ try {
+ val captureBuilder =
+ session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+ captureBuilder.addTarget(previewSurface)
+ session.setRepeatingRequest(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(), captureCallbacks
+ )
+ cont.resume(session)
+ runOnUiThread { enableUiControl(true) }
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(
+ RuntimeException("Failed to create capture session.")
+ )
+ }
+ }
+
+ override fun onConfigureFailed(session: CameraExtensionSession) {
+ Log.e(TAG, "CaptureSession - onConfigureFailed: $session")
+ cont.resumeWithException(
+ RuntimeException("Configure failed when creating capture session.")
+ )
+ }
+ }
+ )
+ try {
+ cameraDevice!!.createExtensionSession(extensionConfiguration)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, e.toString())
+ cont.resumeWithException(RuntimeException("Failed to create capture session."))
+ }
+ }
+
+ @Suppress("DEPRECATION") /* defaultDisplay */
+ private fun setupPreview() {
+ if (!textureView.isAvailable) {
+ Toast.makeText(
+ this, "TextureView is invalid!!",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ val previewResolution = pickPreviewResolution(
+ cameraManager,
+ currentCameraId,
+ resources.displayMetrics,
+ currentExtensionMode
+ )
+
+ if (previewResolution == null) {
+ Toast.makeText(
+ this,
+ "Invalid preview extension sizes!.",
+ Toast.LENGTH_SHORT
+ ).show()
+ finish()
+ return
+ }
+
+ textureView.surfaceTexture?.setDefaultBufferSize(
+ previewResolution.width,
+ previewResolution.height
+ )
+ transformPreview(textureView, previewResolution, windowManager.defaultDisplay.rotation)
+ }
+
+ private fun setupImageReader(): ImageReader {
+ val (size, format) = pickStillImageResolution(
+ extensionCharacteristics,
+ currentExtensionMode
+ )
+
+ return ImageReader.newInstance(size.width, size.height, format, 1)
+ }
+
+ /**
+ * Takes a picture.
+ */
+ private fun takePicture() = lifecycleScope.launch(cameraTaskDispatcher) {
+ Preconditions.checkState(
+ cameraExtensionSession != null,
+ "take picture button is only enabled when session is configured successfully"
+ )
+ val session = cameraExtensionSession!!
+
+ var takePictureCompleter: Completer<Any?>? = null
+
+ imageSaveTerminationFuture = CallbackToFutureAdapter.getFuture<Any?> {
+ takePictureCompleter = it
+ "imageSaveTerminationFuture"
+ }
+
+ stillImageReader!!.setOnImageAvailableListener(
+ { reader: ImageReader ->
+ lifecycleScope.launch(cameraTaskDispatcher) {
+ acquireImageAndSave(reader)
+ stillImageReader!!.setOnImageAvailableListener(null, null)
+ takePictureCompleter?.set(null)
+ lifecycleScope.launch(Dispatchers.Main) {
+ enableUiControl(true)
+ }
+ }
+ }, Handler(Looper.getMainLooper())
+ )
+
+ val captureBuilder = session.device.createCaptureRequest(
+ CameraDevice.TEMPLATE_STILL_CAPTURE
+ )
+ captureBuilder.addTarget(stillImageReader!!.surface)
+
+ session.capture(
+ captureBuilder.build(),
+ cameraTaskDispatcher.asExecutor(),
+ object : CameraExtensionSession.ExtensionCaptureCallback() {
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ takePictureCompleter?.set(null)
+ Log.e(TAG, "Failed to take picture.")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+ }
+ )
+ }
+
+ /**
+ * Acquires the latest image from the image reader and save it to the Pictures folder
+ */
+ private fun acquireImageAndSave(imageReader: ImageReader) {
+ try {
+ val formatter: Format =
+ SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US)
+ val fileName =
+ "[${formatter.format(Calendar.getInstance().time)}][Camera2]${
+ getExtensionModeStringFromId(currentExtensionMode)
+ }"
+
+ val rotationDegrees = calculateRelativeImageRotationDegrees(
+ (getDisplayRotationDegrees(display!!.rotation)),
+ cameraSensorRotationDegrees,
+ currentCameraId == backCameraId
+ )
+
+ imageReader.acquireLatestImage().let { image ->
+ val uri = FileUtil.saveImage(
+ image,
+ fileName,
+ ".jpg",
+ "Pictures/ExtensionsPictures",
+ contentResolver,
+ rotationDegrees
+ )
+
+ image.close()
+
+ val msg = if (uri != null) {
+ "Saved image to $fileName.jpg"
+ } else {
+ "Failed to save image."
+ }
+
+ lifecycleScope.launch(Dispatchers.Main) {
+ Toast.makeText(this@Camera2ExtensionsActivity, msg, Toast.LENGTH_SHORT).show()
+ }
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, e.toString())
+ }
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ val inflater = menuInflater
+ inflater.inflate(R.menu.main_menu_camera2_extensions_activity, menu)
+
+ return true
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ R.id.menu_camerax_extensions -> {
+ closeCameraAndStartActivity(CameraExtensionsActivity::class.java.name)
+ return true
+ }
+ R.id.menu_validation_tool -> {
+ closeCameraAndStartActivity(CameraValidationResultActivity::class.java.name)
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun closeCameraAndStartActivity(className: String) {
+ // Needs to close the camera first. Otherwise, the next activity might be failed to open
+ // the camera and configure the capture session.
+ runBlocking {
+ closeCaptureSession().await()
+ closeCamera().await()
+ }
+
+ val intent = Intent()
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
+ intent.setClassName(this, className)
+ startActivity(intent)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5a0c0ce..47d80f8 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -35,6 +35,7 @@
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
+import android.view.ViewStub;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
@@ -273,8 +274,8 @@
captureButton.setOnClickListener((view) -> {
resetTakePictureIdlingResource();
- String fileName = formatter.format(Calendar.getInstance().getTime())
- + extensionModeString + ".jpg";
+ String fileName = "[" + formatter.format(Calendar.getInstance().getTime())
+ + "][CameraX]" + extensionModeString + ".jpg";
File saveFile = new File(dir, fileName);
ImageCapture.OutputFileOptions outputFileOptions;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
@@ -332,9 +333,9 @@
sendBroadcast(intent);
}
- Toast.makeText(getApplicationContext(),
- "Saved image to " + saveFile,
- Toast.LENGTH_SHORT).show();
+ Toast.makeText(CameraExtensionsActivity.this,
+ "Saved image to " + fileName,
+ Toast.LENGTH_LONG).show();
}
}
@@ -383,7 +384,9 @@
StrictMode.VmPolicy policy =
new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build();
StrictMode.setVmPolicy(policy);
- mPreviewView = findViewById(R.id.previewView);
+ ViewStub viewFinderStub = findViewById(R.id.viewFinderStub);
+ viewFinderStub.setLayoutResource(R.layout.full_previewview);
+ mPreviewView = (PreviewView) viewFinderStub.inflate();
mFrameInfo = findViewById(R.id.frameInfo);
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
setupPinchToZoomAndTapToFocus(mPreviewView);
@@ -427,19 +430,36 @@
@Override
public boolean onCreateOptionsMenu(@Nullable Menu menu) {
- MenuInflater inflater = getMenuInflater();
- inflater.inflate(R.menu.main_menu, menu);
+ if (menu != null) {
+ MenuInflater inflater = getMenuInflater();
+ inflater.inflate(R.menu.main_menu, menu);
+
+ // Remove Camera2Extensions implementation entry if the device API level is less than 32
+ if (Build.VERSION.SDK_INT < 31) {
+ menu.removeItem(R.id.menu_camera2_extensions);
+ }
+ }
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
- if (item.getItemId() == R.id.menu_validation_tool) {
- Intent intent = new Intent(this, CameraValidationResultActivity.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(intent);
- finish();
- return true;
+ Intent intent = new Intent();
+ intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
+ switch (item.getItemId()) {
+ case R.id.menu_camera2_extensions:
+ if (Build.VERSION.SDK_INT >= 31) {
+ mCameraProvider.unbindAll();
+ intent.setClassName(this, Camera2ExtensionsActivity.class.getName());
+ startActivity(intent);
+ finish();
+ }
+ return true;
+ case R.id.menu_validation_tool:
+ intent.setClassName(this, CameraValidationResultActivity.class.getName());
+ startActivity(intent);
+ finish();
+ return true;
}
return super.onOptionsItemSelected(item);
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
new file mode 100644
index 0000000..8bbd835
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/Camera2ExtensionsUtil.kt
@@ -0,0 +1,347 @@
+/*
+ * 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.integration.extensions.utils
+
+import android.annotation.SuppressLint
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.graphics.Point
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraExtensionCharacteristics
+import android.hardware.camera2.CameraExtensionSession
+import android.hardware.camera2.CameraExtensionSession.ExtensionCaptureCallback
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CaptureRequest
+import android.os.Build
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.Size
+import android.view.Surface
+import android.view.TextureView
+import androidx.annotation.RequiresApi
+import java.math.BigDecimal
+import java.math.RoundingMode
+import java.util.stream.Collectors
+
+private const val TAG = "Camera2ExtensionsUtil"
+
+/**
+ * Util functions for Camera2 Extensions implementation
+ */
+object Camera2ExtensionsUtil {
+
+ /**
+ * Converts extension mode from integer to string.
+ */
+ @Suppress("DEPRECATION") // EXTENSION_BEAUTY
+ @JvmStatic
+ fun getExtensionModeStringFromId(extension: Int): String {
+ return when (extension) {
+ CameraExtensionCharacteristics.EXTENSION_HDR -> "HDR"
+ CameraExtensionCharacteristics.EXTENSION_NIGHT -> "NIGHT"
+ CameraExtensionCharacteristics.EXTENSION_BOKEH -> "BOKEH"
+ CameraExtensionCharacteristics.EXTENSION_BEAUTY -> "FACE RETOUCH"
+ else -> "AUTO"
+ }
+ }
+
+ /**
+ * Gets the first camera id of the specified lens facing.
+ */
+ @JvmStatic
+ fun getLensFacingCameraId(cameraManager: CameraManager, lensFacing: Int): String {
+ cameraManager.cameraIdList.forEach { cameraId ->
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ if (characteristics[CameraCharacteristics.LENS_FACING] == lensFacing) {
+ characteristics[CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES]?.let {
+ if (it.contains(
+ CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE
+ )
+ ) {
+ return cameraId
+ }
+ }
+ }
+ }
+
+ throw IllegalArgumentException("Can't find camera of lens facing $lensFacing")
+ }
+
+ /**
+ * Creates a default extension capture callback implementation.
+ */
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun createExtensionCaptureCallback(): ExtensionCaptureCallback {
+ return object : ExtensionCaptureCallback() {
+ override fun onCaptureStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest,
+ timestamp: Long
+ ) {
+ }
+
+ override fun onCaptureProcessStarted(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ }
+
+ override fun onCaptureFailed(
+ session: CameraExtensionSession,
+ request: CaptureRequest
+ ) {
+ Log.v(TAG, "onCaptureProcessFailed")
+ }
+
+ override fun onCaptureSequenceCompleted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceCompleted: $sequenceId")
+ }
+
+ override fun onCaptureSequenceAborted(
+ session: CameraExtensionSession,
+ sequenceId: Int
+ ) {
+ Log.v(TAG, "onCaptureProcessSequenceAborted: $sequenceId")
+ }
+ }
+ }
+
+ /**
+ * Picks a preview resolution that is both close/same as the display size and supported by camera
+ * and extensions.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickPreviewResolution(
+ cameraManager: CameraManager,
+ cameraId: String,
+ displayMetrics: DisplayMetrics,
+ extensionMode: Int
+ ): Size? {
+ val characteristics = cameraManager.getCameraCharacteristics(cameraId)
+ val map = characteristics.get(
+ CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
+ )
+ val textureSizes = map!!.getOutputSizes(
+ SurfaceTexture::class.java
+ )
+ val displaySize = Point()
+ displaySize.x = displayMetrics.widthPixels
+ displaySize.y = displayMetrics.heightPixels
+ if (displaySize.x < displaySize.y) {
+ displaySize.x = displayMetrics.heightPixels
+ displaySize.y = displayMetrics.widthPixels
+ }
+ val displayArRatio = displaySize.x.toFloat() / displaySize.y
+ val previewSizes = ArrayList<Size>()
+ for (sz in textureSizes) {
+ val arRatio = sz.width.toFloat() / sz.height
+ if (Math.abs(arRatio - displayArRatio) <= .2f) {
+ previewSizes.add(sz)
+ }
+ }
+ val extensionCharacteristics = cameraManager.getCameraExtensionCharacteristics(cameraId)
+ val extensionSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, SurfaceTexture::class.java
+ )
+ if (extensionSizes.isEmpty()) {
+ return null
+ }
+
+ var previewSize = extensionSizes[0]
+ val supportedPreviewSizes =
+ previewSizes.stream().distinct().filter { o: Size -> extensionSizes.contains(o) }
+ .collect(Collectors.toList())
+ if (supportedPreviewSizes.isNotEmpty()) {
+ var currentDistance = Int.MAX_VALUE
+ for (sz in supportedPreviewSizes) {
+ val distance = Math.abs(sz.width * sz.height - displaySize.x * displaySize.y)
+ if (currentDistance > distance) {
+ currentDistance = distance
+ previewSize = sz
+ }
+ }
+ } else {
+ Log.w(
+ TAG, "No overlap between supported camera and extensions preview sizes using" +
+ " first available!"
+ )
+ }
+
+ return previewSize
+ }
+
+ /**
+ * Picks a resolution for still image capture.
+ */
+ @SuppressLint("ClassVerificationFailure")
+ @RequiresApi(Build.VERSION_CODES.S)
+ @JvmStatic
+ fun pickStillImageResolution(
+ extensionCharacteristics: CameraExtensionCharacteristics,
+ extensionMode: Int
+ ): Pair<Size, Int> {
+ val yuvColorEncodingSystemSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.YUV_420_888
+ )
+ val jpegSizes = extensionCharacteristics.getExtensionSupportedSizes(
+ extensionMode, ImageFormat.JPEG
+ )
+ val stillFormat = if (jpegSizes.isEmpty()) ImageFormat.YUV_420_888 else ImageFormat.JPEG
+ val stillCaptureSize =
+ if (jpegSizes.isEmpty()) yuvColorEncodingSystemSizes[0] else jpegSizes[0]
+
+ return Pair(stillCaptureSize, stillFormat)
+ }
+
+ /**
+ * Transforms the texture view to display the content of resolution in correct direction and
+ * aspect ratio.
+ */
+ @JvmStatic
+ fun transformPreview(textureView: TextureView, resolution: Size, displayRotation: Int) {
+ if (resolution.width == 0 || resolution.height == 0) {
+ return
+ }
+ if (textureView.width == 0 || textureView.height == 0) {
+ return
+ }
+ val matrix = Matrix()
+ val left: Int = textureView.left
+ val right: Int = textureView.right
+ val top: Int = textureView.top
+ val bottom: Int = textureView.bottom
+
+ // Compute the preview ui size based on the available width, height, and ui orientation.
+ val viewWidth = right - left
+ val viewHeight = bottom - top
+ val displayRotationDegrees: Int = getDisplayRotationDegrees(displayRotation)
+ val scaled: Size = calculatePreviewViewDimens(
+ resolution, viewWidth, viewHeight, displayRotation
+ )
+
+ // Compute the center of the view.
+ val centerX = (viewWidth / 2).toFloat()
+ val centerY = (viewHeight / 2).toFloat()
+
+ // Do corresponding rotation to correct the preview direction
+ matrix.postRotate((-displayRotationDegrees).toFloat(), centerX, centerY)
+
+ // Compute the scale value for center crop mode
+ var xScale = scaled.width / viewWidth.toFloat()
+ var yScale = scaled.height / viewHeight.toFloat()
+ if (displayRotationDegrees % 180 == 90) {
+ xScale = scaled.width / viewHeight.toFloat()
+ yScale = scaled.height / viewWidth.toFloat()
+ }
+
+ // Only two digits after the decimal point are valid for postScale. Need to get ceiling of
+ // two digits floating value to do the scale operation. Otherwise, the result may be scaled
+ // not large enough and will have some blank lines on the screen.
+ xScale = BigDecimal(xScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+ yScale = BigDecimal(yScale.toDouble()).setScale(2, RoundingMode.CEILING).toFloat()
+
+ // Do corresponding scale to resolve the deformation problem
+ matrix.postScale(xScale, yScale, centerX, centerY)
+ textureView.setTransform(matrix)
+ }
+
+ /**
+ * Converts the display rotation to degrees value.
+ *
+ * @return One of 0, 90, 180, 270.
+ */
+ @JvmStatic
+ fun getDisplayRotationDegrees(displayRotation: Int): Int = when (displayRotation) {
+ Surface.ROTATION_0 -> 0
+ Surface.ROTATION_90 -> 90
+ Surface.ROTATION_180 -> 180
+ Surface.ROTATION_270 -> 270
+ else -> throw UnsupportedOperationException(
+ "Unsupported display rotation: $displayRotation"
+ )
+ }
+
+ /**
+ * Calculates the delta between a source rotation and destination rotation.
+ *
+ * <p>A typical use of this method would be calculating the angular difference between the
+ * display orientation (destRotationDegrees) and camera sensor orientation
+ * (sourceRotationDegrees).
+ *
+ * @param destRotationDegrees The destination rotation relative to the device's natural
+ * rotation.
+ * @param sourceRotationDegrees The source rotation relative to the device's natural rotation.
+ * @param isOppositeFacing Whether the source and destination planes are facing opposite
+ * directions.
+ */
+ @JvmStatic
+ fun calculateRelativeImageRotationDegrees(
+ destRotationDegrees: Int,
+ sourceRotationDegrees: Int,
+ isOppositeFacing: Boolean
+ ): Int {
+ val result: Int = if (isOppositeFacing) {
+ (sourceRotationDegrees - destRotationDegrees + 360) % 360
+ } else {
+ (sourceRotationDegrees + destRotationDegrees) % 360
+ }
+
+ return result
+ }
+
+ /**
+ * Calculates the preview size which can display the source image in correct aspect ratio.
+ */
+ @JvmStatic
+ private fun calculatePreviewViewDimens(
+ srcSize: Size,
+ parentWidth: Int,
+ parentHeight: Int,
+ displayRotation: Int
+ ): Size {
+ var inWidth = srcSize.width
+ var inHeight = srcSize.height
+ if (displayRotation == 0 || displayRotation == 180) {
+ // Need to reverse the width and height since we're in landscape orientation.
+ inWidth = srcSize.height
+ inHeight = srcSize.width
+ }
+ var outWidth = parentWidth
+ var outHeight = parentHeight
+ if (inWidth != 0 && inHeight != 0) {
+ val vfRatio = inWidth / inHeight.toFloat()
+ val parentRatio = parentWidth / parentHeight.toFloat()
+
+ // Match shortest sides together.
+ if (vfRatio < parentRatio) {
+ outWidth = parentWidth
+ outHeight = Math.round(parentWidth / vfRatio)
+ } else {
+ outWidth = Math.round(parentHeight * vfRatio)
+ outHeight = parentHeight
+ }
+ }
+ return Size(outWidth, outHeight)
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
new file mode 100644
index 0000000..dbc661d
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/FileUtil.kt
@@ -0,0 +1,190 @@
+/*
+ * 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.integration.extensions.utils
+
+import android.content.ContentResolver
+import android.content.ContentValues
+import android.graphics.ImageFormat
+import android.media.Image
+import android.net.Uri
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import androidx.camera.core.impl.utils.Exif
+import androidx.core.net.toFile
+import androidx.core.net.toUri
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+
+private const val TAG = "FileUtil"
+
+/**
+ * File util functions
+ */
+object FileUtil {
+
+ /**
+ * Saves an [Image] to the specified file path. The format of the input [Image] must be JPEG or
+ * YUV_420_888 format.
+ */
+ @JvmStatic
+ fun saveImage(
+ image: Image,
+ fileNamePrefix: String,
+ fileNameSuffix: String,
+ relativePath: String,
+ contentResolver: ContentResolver,
+ rotationDegrees: Int
+ ): Uri? {
+ require((image.format == ImageFormat.JPEG) or (image.format == ImageFormat.YUV_420_888)) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val fileName = if (fileNameSuffix.isNotEmpty() && fileNameSuffix[0] == '.') {
+ fileNamePrefix + fileNameSuffix
+ } else {
+ "$fileNamePrefix.$fileNameSuffix"
+ }
+
+ // Saves the image to the temp file
+ val tempFileUri =
+ saveImageToTempFile(image, fileNamePrefix, fileNameSuffix) ?: return null
+
+ // Updates Exif rotation tag info
+ val exif = Exif.createFromFile(tempFileUri.toFile())
+ exif.rotate(rotationDegrees)
+ exif.save()
+
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
+ put(MediaStore.MediaColumns.RELATIVE_PATH, relativePath)
+ }
+
+ // Copies the temp file to the final output path
+ return copyTempFileToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+ }
+
+ /**
+ * Saves an [Image] to a temp file.
+ */
+ @JvmStatic
+ fun saveImageToTempFile(
+ image: Image,
+ prefix: String,
+ suffix: String,
+ cacheDir: File? = null
+ ): Uri? {
+ val tempFile = File.createTempFile(
+ prefix,
+ suffix,
+ cacheDir
+ )
+
+ val byteArray = when (image.format) {
+ ImageFormat.JPEG -> {
+ ImageUtil.jpegImageToJpegByteArray(image)
+ }
+ ImageFormat.YUV_420_888 -> {
+ ImageUtil.yuvImageToJpegByteArray(image, 100)
+ }
+ else -> {
+ Log.e(TAG, "Incorrect image format of the input image proxy: ${image.format}")
+ return null
+ }
+ }
+
+ val outputStream = FileOutputStream(tempFile)
+ outputStream.write(byteArray)
+ outputStream.close()
+
+ return tempFile.toUri()
+ }
+
+ /**
+ * Copies temp file to the destination location.
+ *
+ * @return null if the copy process is failed.
+ */
+ @JvmStatic
+ fun copyTempFileToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ targetUrl: Uri,
+ contentValues: ContentValues,
+ ): Uri? {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ Log.e(TAG, "The known devices which support Extensions should be at least" +
+ " Android Q!")
+ return null
+ }
+
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
+
+ val outputUri = contentResolver.insert(targetUrl, contentValues) ?: return null
+
+ if (copyTempFileByteArrayToOutputLocation(
+ contentResolver,
+ tempFileUri,
+ outputUri
+ )
+ ) {
+ contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
+ contentResolver.update(outputUri, contentValues, null, null)
+ return outputUri
+ } else {
+ Log.e(TAG, "Failed to copy the temp file to the output path!")
+ }
+
+ return null
+ }
+
+ /**
+ * Copies temp file byte array to output [Uri].
+ *
+ * @return false if the [Uri] is not writable.
+ */
+ @JvmStatic
+ private fun copyTempFileByteArrayToOutputLocation(
+ contentResolver: ContentResolver,
+ tempFileUri: Uri,
+ uri: Uri
+ ): Boolean {
+ contentResolver.openOutputStream(uri).use { outputStream ->
+ if (tempFileUri.path == null || outputStream == null) {
+ return false
+ }
+
+ val tempFile = File(tempFileUri.path!!)
+
+ FileInputStream(tempFile).use { inputStream ->
+ val buf = ByteArray(1024)
+ var len: Int
+ while (inputStream.read(buf).also { len = it } > 0) {
+ outputStream.write(buf, 0, len)
+ }
+ }
+ }
+ return true
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
new file mode 100644
index 0000000..70c1cb5
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/utils/ImageUtil.kt
@@ -0,0 +1,145 @@
+/*
+ * 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.integration.extensions.utils
+
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.graphics.YuvImage
+import android.media.Image
+import androidx.annotation.IntRange
+import androidx.camera.core.ImageProxy
+import java.io.ByteArrayOutputStream
+
+/**
+ * Image util functions
+ */
+object ImageUtil {
+
+ /**
+ * Converts JPEG [Image] to [ByteArray]
+ */
+ @JvmStatic
+ fun jpegImageToJpegByteArray(image: Image): ByteArray {
+ require(image.format == ImageFormat.JPEG) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ val planes = image.planes
+ val buffer = planes[0].buffer
+ val data = ByteArray(buffer.capacity())
+ buffer.rewind()
+ buffer[data]
+ return data
+ }
+
+ /**
+ * Converts YUV_420_888 [ImageProxy] to JPEG byte array. The input YUV_420_888 image
+ * will be cropped if a non-null crop rectangle is specified. The output JPEG byte array will
+ * be compressed by the specified quality value.
+ */
+ @JvmStatic
+ fun yuvImageToJpegByteArray(
+ image: Image,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+ return nv21ToJpeg(
+ yuv_420_888toNv21(image),
+ image.width,
+ image.height,
+ jpegQuality
+ )
+ }
+
+ /**
+ * Converts nv21 byte array to JPEG format.
+ */
+ @JvmStatic
+ private fun nv21ToJpeg(
+ nv21: ByteArray,
+ width: Int,
+ height: Int,
+ @IntRange(from = 1, to = 100) jpegQuality: Int
+ ): ByteArray {
+ val out = ByteArrayOutputStream()
+ val yuv = YuvImage(nv21, ImageFormat.NV21, width, height, null)
+ val success = yuv.compressToJpeg(Rect(0, 0, width, height), jpegQuality, out)
+
+ if (!success) {
+ throw RuntimeException("YuvImage failed to encode jpeg.")
+ }
+ return out.toByteArray()
+ }
+
+ /**
+ * Converts a YUV [Image] to NV21 byte array.
+ */
+ @JvmStatic
+ private fun yuv_420_888toNv21(image: Image): ByteArray {
+ require(image.format == ImageFormat.YUV_420_888) {
+ "Incorrect image format of the input image proxy: ${image.format}"
+ }
+
+ val yPlane = image.planes[0]
+ val uPlane = image.planes[1]
+ val vPlane = image.planes[2]
+ val yBuffer = yPlane.buffer
+ val uBuffer = uPlane.buffer
+ val vBuffer = vPlane.buffer
+ yBuffer.rewind()
+ uBuffer.rewind()
+ vBuffer.rewind()
+ val ySize = yBuffer.remaining()
+ var position = 0
+ // TODO(b/115743986): Pull these bytes from a pool instead of allocating for every image.
+ val nv21 = ByteArray(ySize + image.width * image.height / 2)
+
+ // Add the full y buffer to the array. If rowStride > 1, some padding may be skipped.
+ for (row in 0 until image.height) {
+ yBuffer[nv21, position, image.width]
+ position += image.width
+ yBuffer.position(
+ Math.min(ySize, yBuffer.position() - image.width + yPlane.rowStride)
+ )
+ }
+ val chromaHeight = image.height / 2
+ val chromaWidth = image.width / 2
+ val vRowStride = vPlane.rowStride
+ val uRowStride = uPlane.rowStride
+ val vPixelStride = vPlane.pixelStride
+ val uPixelStride = uPlane.pixelStride
+
+ // Interleave the u and v frames, filling up the rest of the buffer. Use two line buffers to
+ // perform faster bulk gets from the byte buffers.
+ val vLineBuffer = ByteArray(vRowStride)
+ val uLineBuffer = ByteArray(uRowStride)
+ for (row in 0 until chromaHeight) {
+ vBuffer[vLineBuffer, 0, Math.min(vRowStride, vBuffer.remaining())]
+ uBuffer[uLineBuffer, 0, Math.min(uRowStride, uBuffer.remaining())]
+ var vLineBufferPosition = 0
+ var uLineBufferPosition = 0
+ for (col in 0 until chromaWidth) {
+ nv21[position++] = vLineBuffer[vLineBufferPosition]
+ nv21[position++] = uLineBuffer[uLineBufferPosition]
+ vLineBufferPosition += vPixelStride
+ uLineBufferPosition += uPixelStride
+ }
+ }
+ return nv21
+ }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
index 33c44a1..013ef13 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageCaptureActivity.kt
@@ -19,7 +19,6 @@
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.Configuration
-import android.graphics.ImageFormat
import android.os.Bundle
import android.util.Log
import android.view.GestureDetector
@@ -30,11 +29,13 @@
import android.widget.Button
import android.widget.ImageButton
import android.widget.Toast
+import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.Camera
import androidx.camera.core.CameraControl
import androidx.camera.core.CameraInfo
import androidx.camera.core.DisplayOrientedMeteringPointFactory
+import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.FocusMeteringAction
import androidx.camera.core.FocusMeteringResult
import androidx.camera.core.ImageCapture
@@ -51,6 +52,7 @@
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -66,14 +68,11 @@
import androidx.concurrent.futures.await
import androidx.core.content.ContextCompat
import androidx.core.math.MathUtils
-import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch
-import java.io.File
-import java.io.FileOutputStream
private const val TAG = "ImageCaptureActivity"
@@ -196,6 +195,7 @@
}
}
+ @OptIn(markerClass = [ExperimentalGetImage::class])
private fun setupUiControls() {
// Sets up the flash toggle button
setUpFlashButton()
@@ -227,21 +227,22 @@
} else {
"$filenamePrefix[Disabled]"
}
- val tempFile = File.createTempFile(
- filename,
- "",
- codeCacheDir
- )
- val outputStream = FileOutputStream(tempFile)
- val byteArray = jpegImageToJpegByteArray(image)
- outputStream.write(byteArray)
- outputStream.close()
- result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, tempFile.toUri())
- result.putExtra(
- INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
- image.imageInfo.rotationDegrees
- )
+ val uri =
+ FileUtil.saveImageToTempFile(image.image!!, filename, "", cacheDir)
+
+ if (uri == null) {
+ result.putExtra(
+ INTENT_EXTRA_KEY_ERROR_CODE,
+ ERROR_CODE_SAVE_IMAGE_FAILED
+ )
+ } else {
+ result.putExtra(INTENT_EXTRA_KEY_IMAGE_URI, uri)
+ result.putExtra(
+ INTENT_EXTRA_KEY_IMAGE_ROTATION_DEGREES,
+ image.imageInfo.rotationDegrees
+ )
+ }
finish()
}
@@ -456,25 +457,11 @@
extensionToggleButton.setImageResource(resourceId)
}
- /**
- * Converts JPEG [ImageProxy] to JPEG byte array.
- */
- internal fun jpegImageToJpegByteArray(image: ImageProxy): ByteArray {
- require(image.format == ImageFormat.JPEG) {
- "Incorrect image format of the input image proxy: ${image.format}"
- }
- val planes = image.planes
- val buffer = planes[0].buffer
- val data = ByteArray(buffer.capacity())
- buffer.rewind()
- buffer[data]
- return data
- }
-
companion object {
const val ERROR_CODE_NONE = 0
const val ERROR_CODE_BIND_FAIL = 1
const val ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT = 2
const val ERROR_CODE_TAKE_PICTURE_FAILED = 3
+ const val ERROR_CODE_SAVE_IMAGE_FAILED = 4
}
}
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
index 5e01f7a..2782d05 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/ImageValidationActivity.kt
@@ -35,6 +35,7 @@
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.integration.extensions.R
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_CAMERA_ID
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_ERROR_CODE
import androidx.camera.integration.extensions.validation.CameraValidationResultActivity.Companion.INTENT_EXTRA_KEY_EXTENSION_MODE
@@ -48,13 +49,13 @@
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_BIND_FAIL
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_NONE
+import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_SAVE_IMAGE_FAILED
import androidx.camera.integration.extensions.validation.ImageCaptureActivity.Companion.ERROR_CODE_TAKE_PICTURE_FAILED
import androidx.camera.integration.extensions.validation.PhotoFragment.Companion.decodeImageToBitmap
import androidx.camera.integration.extensions.validation.TestResults.Companion.INVALID_EXTENSION_MODE
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_FAILED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_NOT_TESTED
import androidx.camera.integration.extensions.validation.TestResults.Companion.TEST_RESULT_PASSED
-import androidx.camera.integration.extensions.validation.TestResults.Companion.copyTempFileToOutputLocation
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
@@ -127,7 +128,8 @@
// Returns with error
if (errorCode == ERROR_CODE_BIND_FAIL ||
errorCode == ERROR_CODE_EXTENSION_MODE_NOT_SUPPORT ||
- errorCode == ERROR_CODE_TAKE_PICTURE_FAILED
+ errorCode == ERROR_CODE_TAKE_PICTURE_FAILED ||
+ errorCode == ERROR_CODE_SAVE_IMAGE_FAILED
) {
result.putExtra(INTENT_EXTRA_KEY_TEST_RESULT, TEST_RESULT_FAILED)
Log.e(TAG, "Failed to take a picture with error code: $errorCode")
@@ -196,13 +198,14 @@
put(MediaStore.MediaColumns.RELATIVE_PATH, "Pictures/ExtensionsValidation")
}
- if (copyTempFileToOutputLocation(
- contentResolver,
- imageUris[viewPager.currentItem].first,
- MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
- contentValues
- )
- ) {
+ val outputUri = copyTempFileToOutputLocation(
+ contentResolver,
+ imageUris[viewPager.currentItem].first,
+ MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
+ contentValues
+ )
+
+ if (outputUri != null) {
Toast.makeText(
this,
"Image is saved as Pictures/ExtensionsValidation/$savedFileName.",
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
index c4becdf4..1a6fa1b 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/validation/TestResults.kt
@@ -20,8 +20,6 @@
import android.content.ContentValues
import android.content.Context
import android.hardware.camera2.CameraCharacteristics
-import android.net.Uri
-import android.os.Build
import android.os.Environment.DIRECTORY_DOCUMENTS
import android.provider.MediaStore
import android.util.Log
@@ -33,6 +31,7 @@
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeIdFromString
import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId
+import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.net.toUri
import java.io.BufferedReader
@@ -127,7 +126,7 @@
testResultsFile.toUri(),
MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL),
contentValues
- )
+ ) != null
) {
return "$DIRECTORY_DOCUMENTS/ExtensionsValidation/$savedFileName"
}
@@ -240,72 +239,6 @@
}
companion object {
-
- /**
- * Copies temp file to the destination location.
- *
- * @return false if the copy process is failed.
- */
- fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- targetUrl: Uri,
- contentValues: ContentValues,
- ): Boolean {
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
- Log.e(TAG, "The known devices which support Extensions should be at least" +
- " Android Q!")
- return false
- }
-
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 1)
-
- val outputUri = contentResolver.insert(targetUrl, contentValues)
-
- if (outputUri != null && copyTempFileToOutputLocation(
- contentResolver,
- tempFileUri,
- outputUri
- )
- ) {
- contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
- contentResolver.update(outputUri, contentValues, null, null)
- return true
- } else {
- Log.e(TAG, "Failed to copy the temp file to the output path!")
- }
-
- return false
- }
-
- /**
- * Copies temp file to output [Uri].
- *
- * @return false if the [Uri] is not writable.
- */
- private fun copyTempFileToOutputLocation(
- contentResolver: ContentResolver,
- tempFileUri: Uri,
- uri: Uri
- ): Boolean {
- contentResolver.openOutputStream(uri).use { outputStream ->
- if (tempFileUri.path == null || outputStream == null) {
- return false
- }
-
- val tempFile = File(tempFileUri.path!!)
-
- FileInputStream(tempFile).use { `in` ->
- val buf = ByteArray(1024)
- var len: Int
- while (`in`.read(buf).also { len = it } > 0) {
- outputStream.write(buf, 0, len)
- }
- }
- }
- return true
- }
-
const val INVALID_EXTENSION_MODE = -1
const val TEST_RESULT_NOT_SUPPORTED = -1
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
index 9ed60c5..1a8adfa 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/activity_camera_extensions.xml
@@ -24,14 +24,14 @@
android:layout_height="match_parent"
tools:context="androidx.camera.integration.extensions.CameraExtensionsActivity">
- <androidx.camera.view.PreviewView
- android:id="@+id/previewView"
+ <ViewStub
+ android:id="@+id/viewFinderStub"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toTopOf="parent"
- app:layout_constraintStart_toStartOf="parent"/>
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
new file mode 100644
index 0000000..4c0f530
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_previewview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<androidx.camera.view.PreviewView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
new file mode 100644
index 0000000..18ebf88
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/layout/full_textureview.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ 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.
+ -->
+
+<TextureView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
index dc9d084..7fe8504 100644
--- a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu.xml
@@ -16,6 +16,9 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
+ android:id="@+id/menu_camera2_extensions"
+ android:title="Camera2 Extensions" />
+ <item
android:id="@+id/menu_validation_tool"
android:title="Validation Tool" />
</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
new file mode 100644
index 0000000..bc8bdba
--- /dev/null
+++ b/camera/integration-tests/extensionstestapp/src/main/res/menu/main_menu_camera2_extensions_activity.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/menu_camerax_extensions"
+ android:title="CameraX Extensions" />
+ <item
+ android:id="@+id/menu_validation_tool"
+ android:title="Validation Tool" />
+</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/build.gradle b/camera/integration-tests/uiwidgetstestapp/build.gradle
index 072b0f5..9c8eef1 100644
--- a/camera/integration-tests/uiwidgetstestapp/build.gradle
+++ b/camera/integration-tests/uiwidgetstestapp/build.gradle
@@ -67,6 +67,7 @@
implementation(project(":camera:camera-camera2"))
implementation(project(":camera:camera-lifecycle"))
implementation(project(":camera:camera-view"))
+ implementation(project(":camera:camera-video"))
// Android Support Library
implementation("androidx.appcompat:appcompat:1.2.0")
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
index cc6c150..0f0927f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraNavHost.kt
@@ -16,7 +16,6 @@
package androidx.camera.integration.uiwidgets.compose.ui.navigation
-import androidx.camera.integration.uiwidgets.compose.ui.screen.gallery.GalleryScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.imagecapture.ImageCaptureScreen
import androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture.VideoCaptureScreen
import androidx.compose.runtime.Composable
@@ -42,9 +41,5 @@
composable(ComposeCameraScreen.VideoCapture.name) {
VideoCaptureScreen()
}
-
- composable(ComposeCameraScreen.Gallery.name) {
- GalleryScreen()
- }
}
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
index 8923506..124fe4f 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/navigation/ComposeCameraScreen.kt
@@ -18,7 +18,6 @@
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CameraAlt
-import androidx.compose.material.icons.filled.PhotoLibrary
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.ui.graphics.vector.ImageVector
@@ -32,9 +31,6 @@
),
VideoCapture(
icon = Icons.Filled.Videocam
- ),
- Gallery(
- icon = Icons.Filled.PhotoLibrary
);
companion object {
@@ -42,7 +38,6 @@
return when (route?.substringBefore("/")) {
ImageCapture.name -> ImageCapture
VideoCapture.name -> VideoCapture
- Gallery.name -> Gallery
null -> defaultRoute
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
index 37c0d18..0f237e5 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/components/CameraControlButton.kt
@@ -20,8 +20,10 @@
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
@@ -32,6 +34,7 @@
imageVector: ImageVector,
contentDescription: String,
modifier: Modifier = Modifier,
+ tint: Color = Color.Unspecified,
onClick: () -> Unit
) {
IconButton(
@@ -41,12 +44,24 @@
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
- modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE),
+ tint = tint
)
}
}
@Composable
+fun CameraControlText(
+ text: String,
+ modifier: Modifier = Modifier
+) {
+ Text(
+ text = text,
+ modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE)
+ )
+}
+
+@Composable
fun CameraControlButtonPlaceholder(modifier: Modifier = Modifier) {
Spacer(modifier = modifier.size(CAMERA_CONTROL_BUTTON_SIZE))
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
deleted file mode 100644
index fdc9e2d..0000000
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/gallery/GalleryScreen.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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.integration.uiwidgets.compose.ui.screen.gallery
-
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-
-@Composable
-fun GalleryScreen() {
- Text("Gallery Screen")
-}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
index 726739f..acc60e6 100644
--- a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreen.kt
@@ -16,10 +16,171 @@
package androidx.camera.integration.uiwidgets.compose.ui.screen.videocapture
+import android.view.ViewGroup
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview.SurfaceProvider
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlButton
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlRow
+import androidx.camera.integration.uiwidgets.compose.ui.screen.components.CameraControlText
+import androidx.camera.view.PreviewView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Slider
import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.sharp.FlipCameraAndroid
+import androidx.compose.material.icons.sharp.Lens
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.viewinterop.AndroidView
@Composable
-fun VideoCaptureScreen() {
- Text("Video Capture Screen")
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ state: VideoCaptureScreenState = rememberVideoCaptureScreenState()
+) {
+ val lifecycleOwner = LocalLifecycleOwner.current
+ val localContext = LocalContext.current
+
+ LaunchedEffect(key1 = state.lensFacing) {
+ state.startCamera(context = localContext, lifecycleOwner = lifecycleOwner)
+ }
+
+ VideoCaptureScreen(
+ modifier = modifier,
+ zoomRatio = state.zoomRatio,
+ linearZoom = state.linearZoom,
+ onLinearZoomChange = state::setLinearZoom,
+ isCameraReady = state.isCameraReady,
+ recordState = state.recordState,
+ recordingStatsMsg = state.recordingStatsMsg,
+ onFlipCameraIconClicked = state::toggleLensFacing,
+ onVideoCaptureIconClicked = {
+ state.captureVideo(localContext)
+ },
+ onSurfaceProviderReady = state::setSurfaceProvider,
+ onTouch = state::startTapToFocus
+ )
+}
+
+@Composable
+fun VideoCaptureScreen(
+ modifier: Modifier = Modifier,
+ zoomRatio: Float,
+ linearZoom: Float,
+ onLinearZoomChange: (Float) -> Unit,
+ isCameraReady: Boolean,
+ recordState: VideoCaptureScreenState.RecordState,
+ recordingStatsMsg: String,
+ onFlipCameraIconClicked: () -> Unit,
+ onVideoCaptureIconClicked: () -> Unit,
+ onSurfaceProviderReady: (SurfaceProvider) -> Unit,
+ onTouch: (MeteringPoint) -> Unit
+) {
+ val localContext = LocalContext.current
+
+ val previewView = remember {
+ PreviewView(localContext).apply {
+ layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT
+ )
+
+ onSurfaceProviderReady(this.surfaceProvider)
+
+ setOnTouchListener { view, motionEvent ->
+ val meteringPointFactory = (view as PreviewView).meteringPointFactory
+ val meteringPoint = meteringPointFactory.createPoint(motionEvent.x, motionEvent.y)
+ onTouch(meteringPoint)
+
+ return@setOnTouchListener true
+ }
+ }
+ }
+
+ Box(modifier = modifier.fillMaxSize()) {
+ AndroidView(
+ factory = { previewView }
+ )
+
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ verticalArrangement = Arrangement.Bottom
+ ) {
+
+ // Display Zoom Slider only when Camera is ready
+ if (isCameraReady) {
+ Row(
+ modifier = Modifier.padding(horizontal = 20.dp, vertical = 10.dp),
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ Row(modifier = Modifier.weight(1f)) {
+ Slider(
+ value = linearZoom,
+ onValueChange = onLinearZoomChange
+ )
+ }
+
+ Text(
+ text = "%.2f x".format(zoomRatio),
+ modifier = Modifier
+ .padding(horizontal = 10.dp)
+ .background(Color.White)
+ )
+ }
+ }
+
+ CameraControlRow {
+ CameraControlButton(
+ imageVector = Icons.Sharp.FlipCameraAndroid,
+ contentDescription = "Toggle Camera Lens",
+ onClick = onFlipCameraIconClicked
+ )
+
+ VideoRecordButton(
+ recordState = recordState,
+ onVideoCaptureIconClicked = onVideoCaptureIconClicked
+ )
+
+ CameraControlText(text = recordingStatsMsg)
+ }
+ }
+ }
+}
+
+@Composable
+private fun VideoRecordButton(
+ recordState: VideoCaptureScreenState.RecordState,
+ onVideoCaptureIconClicked: () -> Unit
+) {
+ val iconColor = when (recordState) {
+ VideoCaptureScreenState.RecordState.IDLE -> Color.Black
+ VideoCaptureScreenState.RecordState.RECORDING -> Color.Red
+ VideoCaptureScreenState.RecordState.STOPPING -> Color.Gray
+ }
+
+ CameraControlButton(
+ imageVector = Icons.Sharp.Lens,
+ contentDescription = "Video Capture",
+ modifier = Modifier
+ .padding(1.dp)
+ .border(1.dp, MaterialTheme.colors.onSecondary, CircleShape),
+ tint = iconColor,
+ onClick = onVideoCaptureIconClicked
+ )
}
\ No newline at end of file
diff --git a/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
new file mode 100644
index 0000000..bd6b3cc
--- /dev/null
+++ b/camera/integration-tests/uiwidgetstestapp/src/main/java/androidx/camera/integration/uiwidgets/compose/ui/screen/videocapture/VideoCaptureScreenState.kt
@@ -0,0 +1,322 @@
+/*
+ * 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.integration.uiwidgets.compose.ui.screen.videocapture
+
+import android.Manifest
+import android.content.ContentValues
+import android.content.Context
+import android.os.Build
+import android.provider.MediaStore
+import android.util.Log
+import android.widget.Toast
+import androidx.camera.core.Camera
+import androidx.camera.core.CameraControl
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.FocusMeteringAction
+import androidx.camera.core.MeteringPoint
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.video.MediaStoreOutputOptions
+import androidx.camera.video.Quality
+import androidx.camera.video.QualitySelector
+import androidx.camera.video.Recorder
+import androidx.camera.video.Recording
+import androidx.camera.video.VideoCapture
+import androidx.camera.video.VideoRecordEvent
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_NONE
+import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.concurrent.futures.await
+import androidx.core.content.ContextCompat
+import androidx.core.content.PermissionChecker
+import androidx.lifecycle.LifecycleOwner
+import java.text.SimpleDateFormat
+import java.util.Locale
+import java.util.concurrent.TimeUnit
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.launch
+
+private const val DEFAULT_LENS_FACING = CameraSelector.LENS_FACING_FRONT
+
+class VideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+) {
+ var lensFacing by mutableStateOf(initialLensFacing)
+ private set
+
+ var isCameraReady by mutableStateOf(false)
+ private set
+
+ var linearZoom by mutableStateOf(0f)
+ private set
+
+ var zoomRatio by mutableStateOf(1f)
+ private set
+
+ private var recording: Recording? = null
+
+ var recordState by mutableStateOf(RecordState.IDLE)
+ private set
+
+ var recordingStatsMsg by mutableStateOf("")
+ private set
+
+ private val preview = Preview.Builder().build()
+ private lateinit var recorder: Recorder
+ private lateinit var videoCapture: VideoCapture<Recorder>
+
+ private var camera: Camera? = null
+
+ private val mainScope = MainScope()
+
+ fun setSurfaceProvider(surfaceProvider: Preview.SurfaceProvider) {
+ Log.d(TAG, "Setting Surface Provider")
+ preview.setSurfaceProvider(surfaceProvider)
+ }
+
+ @JvmName("setLinearZoomFunction")
+ fun setLinearZoom(linearZoom: Float) {
+ Log.d(TAG, "Setting Linear Zoom $linearZoom")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set Linear Zoom")
+ return
+ }
+
+ val future = camera!!.cameraControl.setLinearZoom(linearZoom)
+ mainScope.launch {
+ try {
+ future.await()
+ } catch (exc: Exception) {
+ // Log errors not related to CameraControl.OperationCanceledException
+ if (exc !is CameraControl.OperationCanceledException) {
+ Log.w(TAG, "setLinearZoom: $linearZoom failed. ${exc.message}")
+ }
+ }
+ }
+ }
+
+ fun toggleLensFacing() {
+ Log.d(TAG, "Toggling Lens")
+ lensFacing = if (lensFacing == CameraSelector.LENS_FACING_BACK) {
+ CameraSelector.LENS_FACING_FRONT
+ } else {
+ CameraSelector.LENS_FACING_BACK
+ }
+ }
+
+ fun startTapToFocus(meteringPoint: MeteringPoint) {
+ val action = FocusMeteringAction.Builder(meteringPoint).build()
+ camera?.cameraControl?.startFocusAndMetering(action)
+ }
+
+ fun startCamera(context: Context, lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Starting Camera")
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider = cameraProviderFuture.get()
+
+ // Create a new recorder. CameraX currently does not support re-use of Recorder
+ recorder =
+ Recorder.Builder().setQualitySelector(QualitySelector.from(Quality.HIGHEST)).build()
+ videoCapture = VideoCapture.withOutput(recorder)
+
+ val cameraSelector = CameraSelector
+ .Builder()
+ .requireLensFacing(lensFacing)
+ .build()
+
+ // Remove observers from the old camera instance
+ removeZoomStateObservers(lifecycleOwner)
+
+ // Reset internal State of Camera
+ camera = null
+ isCameraReady = false
+
+ try {
+ cameraProvider.unbindAll()
+ val camera = cameraProvider.bindToLifecycle(
+ lifecycleOwner,
+ cameraSelector,
+ preview,
+ videoCapture
+ )
+
+ this.camera = camera
+ setupZoomStateObserver(lifecycleOwner)
+ isCameraReady = true
+ } catch (exc: Exception) {
+ Log.e(TAG, "Use Cases binding failed", exc)
+ }
+ }, ContextCompat.getMainExecutor(context))
+ }
+
+ fun captureVideo(context: Context) {
+ Log.d(TAG, "Capture Video")
+
+ // Disable button if CameraX is already stopping the recording
+ if (recordState == RecordState.STOPPING) {
+ return
+ }
+
+ // Stop current recording session
+ val curRecording = recording
+ if (curRecording != null) {
+ Log.d(TAG, "Recording session exists. Stop recording")
+ recordState = RecordState.STOPPING
+ curRecording.stop()
+ return
+ }
+
+ Log.d(TAG, "Start recording video")
+ val mediaStoreOutputOptions = getMediaStoreOutputOptions(context)
+
+ recording = videoCapture.output
+ .prepareRecording(context, mediaStoreOutputOptions)
+ .apply {
+ val recordAudioPermission = PermissionChecker.checkSelfPermission(
+ context,
+ Manifest.permission.RECORD_AUDIO
+ )
+
+ if (recordAudioPermission == PermissionChecker.PERMISSION_GRANTED) {
+ withAudioEnabled()
+ }
+ }
+ .start(ContextCompat.getMainExecutor(context)) { recordEvent ->
+ // Update record stats
+ val recordingStats = recordEvent.recordingStats
+ val durationMs = TimeUnit.NANOSECONDS.toMillis(recordingStats.recordedDurationNanos)
+ val sizeMb = recordingStats.numBytesRecorded / (1000f * 1000f)
+ val msg = "%.2f s\n%.2f MB".format(durationMs / 1000f, sizeMb)
+ recordingStatsMsg = msg
+
+ when (recordEvent) {
+ is VideoRecordEvent.Start -> {
+ recordState = RecordState.RECORDING
+ }
+ is VideoRecordEvent.Finalize -> {
+ // Once finalized, save the file if it is created
+ val cause = recordEvent.cause
+ when (val errorCode = recordEvent.error) {
+ ERROR_NONE, ERROR_SOURCE_INACTIVE -> { // Save Output
+ val uri = recordEvent.outputResults.outputUri
+ val successMsg = "Video saved at $uri. Code: $errorCode"
+ Log.d(TAG, successMsg, cause)
+ Toast.makeText(context, successMsg, Toast.LENGTH_SHORT).show()
+ }
+ else -> { // Handle Error
+ val failureMsg = "VideoCapture Error($errorCode): $cause"
+ Log.e(TAG, failureMsg, cause)
+ }
+ }
+
+ // Tear down recording
+ recordState = RecordState.IDLE
+ recording = null
+ recordingStatsMsg = ""
+ }
+ }
+ }
+ }
+
+ private fun getMediaStoreOutputOptions(context: Context): MediaStoreOutputOptions {
+ val contentResolver = context.contentResolver
+ val displayName = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
+ .format(System.currentTimeMillis())
+ val contentValues = ContentValues().apply {
+ put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
+ put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
+ put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
+ }
+ }
+
+ return MediaStoreOutputOptions
+ .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
+ .setContentValues(contentValues)
+ .build()
+ }
+
+ private fun setupZoomStateObserver(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Setting up Zoom State Observer")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not ready to set up observer")
+ return
+ }
+
+ removeZoomStateObservers(lifecycleOwner)
+ camera!!.cameraInfo.zoomState.observe(lifecycleOwner) { state ->
+ linearZoom = state.linearZoom
+ zoomRatio = state.zoomRatio
+ }
+ }
+
+ private fun removeZoomStateObservers(lifecycleOwner: LifecycleOwner) {
+ Log.d(TAG, "Removing Observers")
+
+ if (camera == null) {
+ Log.d(TAG, "Camera is not present to remove observers")
+ return
+ }
+
+ camera!!.cameraInfo.zoomState.removeObservers(lifecycleOwner)
+ }
+
+ enum class RecordState {
+ IDLE,
+ RECORDING,
+ STOPPING
+ }
+
+ companion object {
+ private const val TAG = "VideoCaptureScreenState"
+ private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
+ val saver: Saver<VideoCaptureScreenState, *> = listSaver(
+ save = {
+ listOf(it.lensFacing)
+ },
+ restore = {
+ VideoCaptureScreenState(
+ initialLensFacing = it[0]
+ )
+ }
+ )
+ }
+}
+
+@Composable
+fun rememberVideoCaptureScreenState(
+ initialLensFacing: Int = DEFAULT_LENS_FACING
+): VideoCaptureScreenState {
+ return rememberSaveable(
+ initialLensFacing,
+ saver = VideoCaptureScreenState.saver
+ ) {
+ VideoCaptureScreenState(
+ initialLensFacing = initialLensFacing
+ )
+ }
+}
\ No newline at end of file
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
index 6be5b38..ca257d7 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationTransformTests.kt
@@ -984,4 +984,93 @@
@Composable fun Text(s: String) {}
"""
)
+
+ @Test
+ fun memoizeLambdaInsideFunctionReturningValue() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.Composable
+
+ @Composable
+ fun Test(foo: Foo): Int =
+ Consume { foo.value }
+ """,
+ """
+ @Composable
+ fun Test(foo: Foo, %composer: Composer?, %changed: Int): Int {
+ %composer.startReplaceableGroup(<>)
+ sourceInformation(%composer, "C(Test)<{>,<Consum...>:Test.kt")
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ val tmp0 = Consume(remember(foo, {
+ {
+ foo.value
+ }
+ }, %composer, 0b1110 and %changed), %composer, 0)
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ %composer.endReplaceableGroup()
+ return tmp0
+ }
+
+ """.trimIndent(),
+ """
+ import androidx.compose.runtime.Composable
+ import androidx.compose.runtime.Stable
+
+ @Composable
+ fun Consume(block: () -> Int): Int = block()
+
+ @Stable
+ class Foo {
+ val value: Int = 0
+ }
+ """.trimIndent()
+ )
+
+ @Test
+ fun testComposableCaptureInDelegates() = verifyComposeIrTransform(
+ """
+ import androidx.compose.runtime.*
+
+ class Test(val value: Int) : Delegate by Impl({
+ value
+ })
+ """,
+ """
+ @StabilityInferred(parameters = 0)
+ class Test(val value: Int) : Delegate {
+ private val %%delegate_0: Impl = Impl(composableLambdaInstance(<>, true) { %composer: Composer?, %changed: Int ->
+ sourceInformation(%composer, "C:Test.kt")
+ if (%changed and 0b1011 !== 0b0010 || !%composer.skipping) {
+ if (isTraceInProgress()) {
+ traceEventStart(<>, %changed, -1, <>)
+ }
+ value
+ if (isTraceInProgress()) {
+ traceEventEnd()
+ }
+ } else {
+ %composer.skipToGroupEnd()
+ }
+ }
+ )
+ val content: Function2<Composer, Int, Unit>
+ get() {
+ return <this>.%%delegate_0.content
+ }
+ static val %stable: Int = 0
+ }
+ """,
+ """
+ import androidx.compose.runtime.Composable
+
+ interface Delegate {
+ val content: @Composable () -> Unit
+ }
+
+ class Impl(override val content: @Composable () -> Unit) : Delegate
+ """
+ )
}
diff --git a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
index f47eedb..c3410b9 100644
--- a/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
+++ b/compose/compiler/compiler-hosted/src/main/java/androidx/compose/compiler/plugins/kotlin/lower/ComposerLambdaMemoization.kt
@@ -259,15 +259,16 @@
override fun recordCapture(local: IrValueDeclaration?): Boolean {
val isThis = local == thisParam
val isCtorParam = (local?.parent as? IrConstructor)?.parent === declaration
- if (local != null && collectors.isNotEmpty() && isThis) {
+ val isClassParam = isThis || isCtorParam
+ if (local != null && collectors.isNotEmpty() && isClassParam) {
for (collector in collectors) {
collector.recordCapture(local)
}
}
- if (local != null && declaration.isLocal && !isThis && !isCtorParam) {
+ if (local != null && declaration.isLocal && !isClassParam) {
captures.add(local)
}
- return isThis || isCtorParam
+ return isClassParam
}
override fun recordCapture(local: IrSymbolOwner?) { }
override fun pushCollector(collector: CaptureCollector) {
@@ -415,10 +416,7 @@
val composable = declaration.allowsComposableCalls
val canRemember = composable &&
// Don't use remember in an inline function
- !descriptor.isInline &&
- // Don't use remember if in a composable that returns a value
- // TODO(b/150390108): Consider allowing remember in effects
- descriptor.returnType.let { it != null && it.isUnit() }
+ !descriptor.isInline
val context = FunctionContext(declaration, composable, canRemember)
declarationContextStack.push(context)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
index cdecf1d..8c28355 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt
@@ -74,15 +74,11 @@
LazyLayoutItemProvider by LazyLayoutItemProvider(
intervals = intervals,
nearestItemsRange = nearestItemsRange,
- itemContent = itemContentProvider(itemScope)
+ itemContent = { interval: LazyListIntervalContent, index: Int ->
+ interval.item.invoke(itemScope, index)
+ }
)
-// Workaround for compiler crash
-private fun itemContentProvider(itemScope: LazyItemScope) =
- @Composable { interval: LazyListIntervalContent, index: Int ->
- interval.item.invoke(itemScope, index)
- }
-
/**
* We use the idea of sliding window as an optimization, so user can scroll up to this number of
* items until we have to regenerate the key to index map.
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 56e0e66..29a5f39 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -16,8 +16,10 @@
package androidx.compose.foundation.text
+import androidx.compose.ui.text.AnnotatedString
import com.google.common.truth.Truth.assertThat
import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.whenever
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@@ -27,8 +29,14 @@
class TextControllerTest {
@Test
fun `semantics modifier recreated when TextDelegate is set`() {
- val textDelegateBefore = mock<TextDelegate>()
- val textDelegateAfter = mock<TextDelegate>()
+ val textDelegateBefore = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+ }
+
+ val textDelegateAfter = mock<TextDelegate>() {
+ whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+ }
+
// Make sure that mock doesn't do smart memory management:
assertThat(textDelegateAfter).isNotSameInstanceAs(textDelegateBefore)
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
index 9d55522..05d810f 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextSelectionLongPressDragTest.kt
@@ -23,6 +23,7 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.TextLayoutInput
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
@@ -68,6 +69,7 @@
private lateinit var gesture: TextDragObserver
private lateinit var layoutCoordinates: LayoutCoordinates
private lateinit var state: TextState
+ private lateinit var fontFamilyResolver: FontFamily.Resolver
@Before
fun setup() {
@@ -76,8 +78,15 @@
layoutCoordinates = mock {
on { isAttached } doReturn true
}
+ fontFamilyResolver = mock()
- state = TextState(mock(), selectableId)
+ val delegate = TextDelegate(
+ text = AnnotatedString(""),
+ style = TextStyle(),
+ density = Density(1.0f),
+ fontFamilyResolver = fontFamilyResolver
+ )
+ state = TextState(delegate, selectableId)
state.layoutCoordinates = layoutCoordinates
state.layoutResult = TextLayoutResult(
TextLayoutInput(
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
index f5dcd6c..b98fc6fdd 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/LambdaLocationTest.kt
@@ -42,5 +42,7 @@
.isEqualTo(LambdaLocation("TestLambdas.kt", 29, 30))
assertThat(LambdaLocation.resolve(TestLambdas.inlinedParameter))
.isEqualTo(LambdaLocation("TestLambdas.kt", 33, 33))
+ assertThat(LambdaLocation.resolve(TestLambdas.unnamed))
+ .isEqualTo(LambdaLocation("TestLambdas.kt", 35, 35))
}
}
\ No newline at end of file
diff --git a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
index b127774a..5ff2b27 100644
--- a/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
+++ b/compose/ui/ui-inspection/src/androidTest/java/androidx/compose/ui/inspection/testdata/TestLambdas.kt
@@ -32,6 +32,7 @@
val inlinedParameter = { o: IntOffset ->
o.x * 2
}
+ val unnamed: (Int, Int) -> Float = { _, _ -> 0f }
/**
* This inline function will appear at a line numbers
diff --git a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
index 1a52bfb..11e4a1a 100644
--- a/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
+++ b/compose/ui/ui-inspection/src/main/cpp/lambda_location_java_jni.cpp
@@ -169,7 +169,8 @@
InlineRange *ranges = nullptr;
for (int i=0; i<variableCount; i++) {
jvmtiLocalVariableEntry *variable = &variables[i];
- if (strncmp("$i$f$", variable->name, 5) == 0) {
+ char* name = variable->name;
+ if (name != nullptr && strncmp("$i$f$", name, 5) == 0) {
if (ranges == nullptr) {
jvmti->Allocate(sizeof(InlineRange) * (variableCount-i), (unsigned char **)&ranges);
}
diff --git a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
index 62dd84e..43fea44 100644
--- a/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
+++ b/compose/ui/ui-inspection/src/main/java/androidx/compose/ui/inspection/inspector/LayoutInspectorTree.kt
@@ -553,13 +553,10 @@
.flatMap { config -> config.map { RawParameter(it.key.name, it.value) } }
)
- node.mergedSemantics.addAll(
- modifierInfo.asSequence()
- .map { it.modifier }
- .filterIsInstance<SemanticsModifier>()
- .map { it.id }
- .flatMap { semanticsMap[it].orEmpty() }
- )
+ val mergedSemantics = semanticsMap.get(layoutInfo.semanticsId)
+ if (mergedSemantics != null) {
+ node.mergedSemantics.addAll(mergedSemantics)
+ }
node.id = modifierInfo.asSequence()
.map { it.extra }
diff --git a/compose/ui/ui/api/current.ignore b/compose/ui/ui/api/current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/current.ignore
+++ b/compose/ui/ui/api/current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/current.txt b/compose/ui/ui/api/current.txt
index 7996078..6d14189 100644
--- a/compose/ui/ui/api/current.txt
+++ b/compose/ui/ui/api/current.txt
@@ -1906,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1917,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2770,9 +2772,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/public_plus_experimental_current.txt b/compose/ui/ui/api/public_plus_experimental_current.txt
index 707c956..32bc836 100644
--- a/compose/ui/ui/api/public_plus_experimental_current.txt
+++ b/compose/ui/ui/api/public_plus_experimental_current.txt
@@ -2052,6 +2052,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -2063,6 +2064,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2983,9 +2985,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/api/restricted_current.ignore b/compose/ui/ui/api/restricted_current.ignore
index 47b464a..0a42ea8 100644
--- a/compose/ui/ui/api/restricted_current.ignore
+++ b/compose/ui/ui/api/restricted_current.ignore
@@ -1,4 +1,8 @@
// Baseline format: 1.0
+AddedAbstractMethod: androidx.compose.ui.layout.LayoutInfo#getSemanticsId():
+ Added method androidx.compose.ui.layout.LayoutInfo.getSemanticsId()
+
+
RemovedDeprecatedMethod: androidx.compose.ui.graphics.vector.ImageVector.Builder#Builder(String, float, float, float, float, long, int):
Removed deprecated method androidx.compose.ui.graphics.vector.ImageVector.Builder.Builder(String,float,float,float,float,long,int)
RemovedDeprecatedMethod: androidx.compose.ui.input.pointer.PointerInputChange#PointerInputChange(long, long, long, boolean, long, long, boolean, androidx.compose.ui.input.pointer.ConsumedData, int):
diff --git a/compose/ui/ui/api/restricted_current.txt b/compose/ui/ui/api/restricted_current.txt
index 928392d..2b300fe 100644
--- a/compose/ui/ui/api/restricted_current.txt
+++ b/compose/ui/ui/api/restricted_current.txt
@@ -1906,6 +1906,7 @@
method public androidx.compose.ui.unit.LayoutDirection getLayoutDirection();
method public java.util.List<androidx.compose.ui.layout.ModifierInfo> getModifierInfo();
method public androidx.compose.ui.layout.LayoutInfo? getParentInfo();
+ method public int getSemanticsId();
method public androidx.compose.ui.platform.ViewConfiguration getViewConfiguration();
method public int getWidth();
method public boolean isAttached();
@@ -1917,6 +1918,7 @@
property public abstract boolean isPlaced;
property public abstract androidx.compose.ui.unit.LayoutDirection layoutDirection;
property public abstract androidx.compose.ui.layout.LayoutInfo? parentInfo;
+ property public abstract int semanticsId;
property public abstract androidx.compose.ui.platform.ViewConfiguration viewConfiguration;
property public abstract int width;
}
@@ -2806,9 +2808,9 @@
}
@kotlin.jvm.JvmDefaultWithCompatibility public interface SemanticsModifier extends androidx.compose.ui.Modifier.Element {
- method public int getId();
+ method @Deprecated public default int getId();
method public androidx.compose.ui.semantics.SemanticsConfiguration getSemanticsConfiguration();
- property public abstract int id;
+ property @Deprecated public default int id;
property public abstract androidx.compose.ui.semantics.SemanticsConfiguration semanticsConfiguration;
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
index 5adc1c1..c77098c 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/AndroidComposeViewAccessibilityDelegateCompatTest.kt
@@ -904,7 +904,6 @@
val nodes = SemanticsOwner(
LayoutNode().also {
it.modifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
@@ -1264,9 +1263,9 @@
mergeDescendants: Boolean,
properties: (SemanticsPropertyReceiver.() -> Unit)
): SemanticsNode {
- val semanticsModifier = SemanticsModifierCore(id, mergeDescendants, false, properties)
+ val semanticsModifier = SemanticsModifierCore(mergeDescendants, false, properties)
return SemanticsNode(
- SemanticsEntity(InnerPlaceable(LayoutNode()), semanticsModifier),
+ SemanticsEntity(InnerPlaceable(LayoutNode(semanticsId = id)), semanticsModifier),
true
)
}
diff --git a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
index 085a1f7..55e8f7e 100644
--- a/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
+++ b/compose/ui/ui/src/androidAndroidTest/kotlin/androidx/compose/ui/semantics/SemanticsTests.kt
@@ -70,7 +70,6 @@
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import java.util.concurrent.CountDownLatch
import kotlin.math.max
@MediumTest
@@ -91,16 +90,6 @@
isDebugInspectorInfoEnabled = false
}
- private fun executeUpdateBlocking(updateFunction: () -> Unit) {
- val latch = CountDownLatch(1)
- rule.runOnUiThread {
- updateFunction()
- latch.countDown()
- }
-
- latch.await()
- }
-
@Test
fun unchangedSemanticsDoesNotCauseRelayout() {
val layoutCounter = Counter(0)
@@ -118,6 +107,22 @@
}
@Test
+ fun valueSemanticsAreEqual() {
+ assertEquals(
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ },
+ Modifier.semantics {
+ text = AnnotatedString("text")
+ contentDescription = "foo"
+ popup()
+ }
+ )
+ }
+
+ @Test
fun depthFirstPropertyConcat() {
val root = "root"
val child1 = "child1"
@@ -533,6 +538,12 @@
val isAfter = mutableStateOf(false)
+ val content: @Composable () -> Unit = {
+ SimpleTestLayout {
+ nodeCount++
+ }
+ }
+
rule.setContent {
SimpleTestLayout(
Modifier.testTag(TestTag).semantics {
@@ -543,12 +554,9 @@
return@onClick true
}
)
- }
- ) {
- SimpleTestLayout {
- nodeCount++
- }
- }
+ },
+ content = content
+ )
}
// This isn't the important part, just makes sure everything is behaving as expected
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
index a60a1e2..532f0d5 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeView.android.kt
@@ -189,7 +189,6 @@
private set
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
index 130f682..b7ff6d8 100644
--- a/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
+++ b/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidComposeViewAccessibilityDelegateCompat.android.kt
@@ -1558,7 +1558,8 @@
) {
val androidView = view.androidViewsHandler.layoutNodeToHolder[wrapper.layoutNode]
if (androidView == null) {
- virtualViewId = semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.modifier.id)
+ virtualViewId =
+ semanticsNodeIdToAccessibilityVirtualNodeId(wrapper.layoutNode.semanticsId)
}
}
}
@@ -1719,7 +1720,7 @@
?.isMergingSemanticsOfDescendants == true
}?.outerSemantics?.let { semanticsWrapper = it }
}
- val id = semanticsWrapper.modifier.id
+ val id = semanticsWrapper.layoutNode.semanticsId
if (!subtreeChangedSemanticsNodesIds.add(id)) {
return
}
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
index 571029f..cb2fd62 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/LayoutInfo.kt
@@ -77,6 +77,11 @@
* Returns true if this layout is currently a part of the layout tree.
*/
val isAttached: Boolean
+
+ /**
+ * Unique and stable id representing this node to the semantics system.
+ */
+ val semanticsId: Int
}
/**
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
index c973b88..a5b4070 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt
@@ -52,6 +52,7 @@
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.platform.simpleIdentityToString
import androidx.compose.ui.semantics.SemanticsEntity
+import androidx.compose.ui.semantics.SemanticsModifierCore.Companion.generateSemanticsId
import androidx.compose.ui.semantics.outerSemantics
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
@@ -74,7 +75,9 @@
// virtual nodes will be treated as the direct children of the virtual node parent.
// This whole concept will be replaced with a proper subcomposition logic which allows to
// subcompose multiple times into the same LayoutNode and define offsets.
- private val isVirtual: Boolean = false
+ private val isVirtual: Boolean = false,
+ // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode.
+ override val semanticsId: Int = generateSemanticsId()
) : Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode,
Owner.OnLayoutCompletedListener {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
index 164d52e..54e3dfa 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsEntity.kt
@@ -26,6 +26,9 @@
wrapped: LayoutNodeWrapper,
modifier: SemanticsModifier
) : LayoutNodeEntity<SemanticsEntity, SemanticsModifier>(wrapped, modifier) {
+ val id: Int
+ get() = layoutNode.semanticsId
+
private val useMinimumTouchTarget: Boolean
get() = modifier.semanticsConfiguration.getOrNull(SemanticsActions.OnClick) != null
@@ -56,7 +59,7 @@
}
override fun toString(): String {
- return "${super.toString()} id: ${modifier.id} config: ${modifier.semanticsConfiguration}"
+ return "${super.toString()} semanticsId: $id config: ${modifier.semanticsConfiguration}"
}
fun touchBoundsInRoot(): Rect {
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
index 142f6fd..de40d11 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt
@@ -16,10 +16,11 @@
package androidx.compose.ui.semantics
-import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
import androidx.compose.ui.platform.AtomicInt
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.NoInspectorInfo
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.internal.JvmDefaultWithCompatibility
@@ -29,12 +30,12 @@
*/
@JvmDefaultWithCompatibility
interface SemanticsModifier : Modifier.Element {
- /**
- * The unique id of this semantics.
- *
- * Should be generated from SemanticsModifierCore.generateSemanticsId().
- */
- val id: Int
+ @Deprecated(
+ message = "SemanticsModifier.id is now unused and has been set to a fixed value. " +
+ "Retrieve the id from LayoutInfo instead.",
+ replaceWith = ReplaceWith("")
+ )
+ val id: Int get() = -1
/**
* The SemanticsConfiguration holds substantive data, especially a list of key/value pairs
@@ -44,18 +45,18 @@
}
internal class SemanticsModifierCore(
- override val id: Int,
mergeDescendants: Boolean,
clearAndSetSemantics: Boolean,
- properties: (SemanticsPropertyReceiver.() -> Unit)
-) : SemanticsModifier {
+ properties: (SemanticsPropertyReceiver.() -> Unit),
+ inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo
+) : SemanticsModifier, InspectorValueInfo(inspectorInfo) {
override val semanticsConfiguration: SemanticsConfiguration =
SemanticsConfiguration().also {
it.isMergingSemanticsOfDescendants = mergeDescendants
it.isClearingSemantics = clearAndSetSemantics
-
it.properties()
}
+
companion object {
private var lastIdentifier = AtomicInt(0)
fun generateSemanticsId() = lastIdentifier.addAndGet(1)
@@ -64,15 +65,12 @@
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is SemanticsModifierCore) return false
-
- if (id != other.id) return false
if (semanticsConfiguration != other.semanticsConfiguration) return false
-
return true
}
override fun hashCode(): Int {
- return 31 * semanticsConfiguration.hashCode() + id.hashCode()
+ return semanticsConfiguration.hashCode()
}
}
@@ -109,16 +107,16 @@
fun Modifier.semantics(
mergeDescendants: Boolean = false,
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = mergeDescendants,
+ clearAndSetSemantics = false,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "semantics"
this.properties["mergeDescendants"] = mergeDescendants
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants, clearAndSetSemantics = false, properties)
-}
+)
/**
* Clears the semantics of all the descendant nodes and sets new semantics.
@@ -137,12 +135,12 @@
*/
fun Modifier.clearAndSetSemantics(
properties: (SemanticsPropertyReceiver.() -> Unit)
-): Modifier = composed(
+): Modifier = this then SemanticsModifierCore(
+ mergeDescendants = false,
+ clearAndSetSemantics = true,
+ properties = properties,
inspectorInfo = debugInspectorInfo {
name = "clearAndSetSemantics"
this.properties["properties"] = properties
}
-) {
- val id = remember { SemanticsModifierCore.generateSemanticsId() }
- SemanticsModifierCore(id, mergeDescendants = false, clearAndSetSemantics = true, properties)
-}
+)
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
index 1701b58..1a45eb5d 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsNode.kt
@@ -64,7 +64,6 @@
private var fakeNodeParent: SemanticsNode? = null
internal val unmergedConfig = outerSemanticsEntity.collapsedSemanticsConfiguration()
- val id: Int = outerSemanticsEntity.modifier.id
/**
* The [LayoutInfo] that this is associated with.
@@ -81,6 +80,8 @@
*/
internal val layoutNode: LayoutNode = outerSemanticsEntity.layoutNode
+ val id: Int = layoutNode.semanticsId
+
// GEOMETRY
/**
@@ -379,9 +380,12 @@
): SemanticsNode {
val fakeNode = SemanticsNode(
outerSemanticsEntity = SemanticsEntity(
- wrapped = LayoutNode(isVirtual = true).innerLayoutNodeWrapper,
+ wrapped = LayoutNode(
+ isVirtual = true,
+ semanticsId =
+ if (role != null) roleFakeNodeId() else contentDescriptionFakeNodeId()
+ ).innerLayoutNodeWrapper,
modifier = SemanticsModifierCore(
- if (role != null) this.roleFakeNodeId() else contentDescriptionFakeNodeId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = properties
diff --git a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
index c672f75..b420d49 100644
--- a/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
+++ b/compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/platform/SkiaBasedOwner.skiko.kt
@@ -115,7 +115,6 @@
override val sharedDrawScope = LayoutNodeDrawScope()
private val semanticsModifier = SemanticsModifierCore(
- id = SemanticsModifierCore.generateSemanticsId(),
mergeDescendants = false,
clearAndSetSemantics = false,
properties = {}
diff --git a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
index a40a3f6..cd84292 100644
--- a/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
+++ b/compose/ui/ui/src/test/kotlin/androidx/compose/ui/node/LayoutNodeTest.kt
@@ -1335,7 +1335,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode =
@@ -1358,7 +1357,6 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_pointerInputFilterHit_nestedNodes() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val outerNode = LayoutNode(0, 0, 1, 1).apply { attach(MockOwner()) }
@@ -1377,11 +1375,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHit() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
@@ -1439,11 +1435,9 @@
fun hitTestSemantics_pointerInMinimumTouchTarget_closestHitWithOverlap() {
val semanticsConfiguration = SemanticsConfiguration()
val semanticsModifier1 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val semanticsModifier2 = object : SemanticsModifier {
- override val id: Int = 1
override val semanticsConfiguration: SemanticsConfiguration = semanticsConfiguration
}
val layoutNode1 = LayoutNode(0, 0, 5, 5, semanticsModifier1, DpSize(48.dp, 48.dp))
diff --git a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
index e3d6b20..707ae1b 100644
--- a/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
+++ b/datastore/datastore-multiprocess/src/androidTest/java/androidx/datastore/multiprocess/SharedCounterTest.kt
@@ -54,12 +54,18 @@
}
@Test
- fun testCreate_success() {
+ fun testCreate_success_enableMlock() {
val counter: SharedCounter = SharedCounter.create { testFile }
assertThat(counter).isNotNull()
}
@Test
+ fun testCreate_success_disableMlock() {
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
+ assertThat(counter).isNotNull()
+ }
+
+ @Test
fun testCreate_failure() {
val tempFile = tempFolder.newFile()
tempFile.setReadable(false)
@@ -72,13 +78,13 @@
@Test
fun testGetValue() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
assertThat(counter.getValue()).isEqualTo(0)
}
@Test
fun testIncrementAndGet() {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
for (count in 1..100) {
assertThat(counter.incrementAndGetValue()).isEqualTo(count)
}
@@ -86,7 +92,7 @@
@Test
fun testIncrementInParallel() = runTest {
- val counter: SharedCounter = SharedCounter.create { testFile }
+ val counter: SharedCounter = SharedCounter.create(false) { testFile }
val valueToAdd = 100
val numCoroutines = 10
val numbers: MutableSet<Int> = mutableSetOf()
@@ -105,4 +111,20 @@
assertThat(numbers).contains(num)
}
}
+
+ @Test
+ fun testManyInstancesWithMlockDisabled() = runTest {
+ // More than 16
+ val numCoroutines = 5000
+ val counters = mutableListOf<SharedCounter>()
+ val deferred = async {
+ repeat(numCoroutines) {
+ val tempFile = tempFolder.newFile()
+ val counter = SharedCounter.create(false) { tempFile }
+ assertThat(counter.getValue()).isEqualTo(0)
+ counters.add(counter)
+ }
+ }
+ deferred.await()
+ }
}
\ No newline at end of file
diff --git a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
index 892157e..61cddca 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/jni/androidx_datastore_multiprocess_SharedCounter.cc
@@ -35,10 +35,19 @@
extern "C" {
JNIEXPORT jlong JNICALL
-Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeTruncateFile(
JNIEnv *env, jclass clazz, jint fd) {
+ if (int errNum = datastore::TruncateFile(fd)) {
+ return ThrowIoException(env, strerror(errNum));
+ }
+ return 0;
+}
+
+JNIEXPORT jlong JNICALL
+Java_androidx_datastore_multiprocess_NativeSharedCounter_nativeCreateSharedCounter(
+ JNIEnv *env, jclass clazz, jint fd, jboolean enable_mlock) {
void* address = nullptr;
- if (int errNum = datastore::CreateSharedCounter(fd, &address)) {
+ if (int errNum = datastore::CreateSharedCounter(fd, &address, enable_mlock)) {
return ThrowIoException(env, strerror(errNum));
}
return reinterpret_cast<jlong>(address);
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
index 9dcf9da..68f5ce1 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.cc
@@ -39,16 +39,24 @@
namespace datastore {
+int TruncateFile(int fd) {
+ return (ftruncate(fd, NUM_BYTES) == 0) ? 0 : errno;
+}
+
/*
- * This function returns non-zero errno if fails to create the counter. Caller should use
- * "strerror(errno)" to get error message.
+ * This function returns non-zero errno if fails to create the counter. Caller should have called
+ * "TruncateFile" before calling this method. Caller should use "strerror(errno)" to get error
+ * message.
*/
-int CreateSharedCounter(int fd, void** counter_address) {
- if (ftruncate(fd, NUM_BYTES) != 0) {
- return errno;
- }
- void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_LOCKED, fd, 0);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock) {
+ // Map with MAP_SHARED so the memory region is shared with other processes.
+ // MAP_LOCKED may cause memory starvation (b/233902124) so is configurable.
+ int map_flags = MAP_SHARED;
+ // TODO(b/233902124): the impact of MAP_POPULATE is still unclear, experiment
+ // with it when possible.
+ map_flags |= enable_mlock ? MAP_LOCKED : MAP_POPULATE;
+
+ void* mmap_result = mmap(nullptr, NUM_BYTES, PROT_READ | PROT_WRITE, map_flags, fd, 0);
if (mmap_result == MAP_FAILED) {
return errno;
diff --git a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
index 756e2fe..cf73095 100644
--- a/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
+++ b/datastore/datastore-multiprocess/src/main/cpp/shared_counter.h
@@ -21,7 +21,8 @@
#define DATASTORE_SHARED_COUNTER_H
namespace datastore {
-int CreateSharedCounter(int fd, void** counter_address);
+int TruncateFile(int fd);
+int CreateSharedCounter(int fd, void** counter_address, bool enable_mlock);
uint32_t GetCounterValue(std::atomic<uint32_t>* counter);
uint32_t IncrementAndGetCounterValue(std::atomic<uint32_t>* counter);
} // namespace datastore
diff --git a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
index eb64a3c..e01662d 100644
--- a/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
+++ b/datastore/datastore-multiprocess/src/main/java/androidx/datastore/multiprocess/SharedCounter.kt
@@ -25,7 +25,8 @@
* Put the JNI methods in a separate class to make them internal to the package.
*/
internal class NativeSharedCounter {
- external fun nativeCreateSharedCounter(fd: Int): Long
+ external fun nativeTruncateFile(fd: Int): Int
+ external fun nativeCreateSharedCounter(fd: Int, enableMlock: Boolean): Long
external fun nativeGetCounterValue(address: Long): Int
external fun nativeIncrementAndGetCounterValue(address: Long): Int
}
@@ -57,22 +58,28 @@
fun loadLib() = System.loadLibrary("datastore_shared_counter")
@SuppressLint("SyntheticAccessor")
- private fun createCounterFromFd(pfd: ParcelFileDescriptor): SharedCounter {
+ private fun createCounterFromFd(
+ pfd: ParcelFileDescriptor,
+ enableMlock: Boolean
+ ): SharedCounter {
val nativeFd = pfd.getFd()
- val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd)
+ if (nativeSharedCounter.nativeTruncateFile(nativeFd) != 0) {
+ throw IOException("Failed to truncate counter file")
+ }
+ val address = nativeSharedCounter.nativeCreateSharedCounter(nativeFd, enableMlock)
if (address < 0) {
- throw IOException("Failed to mmap or truncate counter file")
+ throw IOException("Failed to mmap counter file")
}
return SharedCounter(address)
}
- internal fun create(produceFile: () -> File): SharedCounter {
+ internal fun create(enableMlock: Boolean = true, produceFile: () -> File): SharedCounter {
val file = produceFile()
return ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_WRITE or ParcelFileDescriptor.MODE_CREATE
).use {
- createCounterFromFd(it)
+ createCounterFromFd(it, enableMlock)
}
}
}
diff --git a/development/JetpadClient.py b/development/JetpadClient.py
index 1e15f07..19b74bc 100644
--- a/development/JetpadClient.py
+++ b/development/JetpadClient.py
@@ -33,7 +33,7 @@
return None
rawJetpadReleaseOutputLines = rawJetpadReleaseOutput.splitlines()
if len(rawJetpadReleaseOutputLines) <= 2:
- print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % args.date)
+ print_e("Error: Date %s returned zero results from Jetpad. Please check your date" % date)
return None
jetpadReleaseOutput = iter(rawJetpadReleaseOutputLines)
return jetpadReleaseOutput
diff --git a/development/auto-version-updater/update_versions_for_release.py b/development/auto-version-updater/update_versions_for_release.py
index 013c163..8ce954a 100755
--- a/development/auto-version-updater/update_versions_for_release.py
+++ b/development/auto-version-updater/update_versions_for_release.py
@@ -19,10 +19,6 @@
import argparse
from datetime import date
import subprocess
-from shutil import rmtree
-from shutil import copyfile
-from distutils.dir_util import copy_tree
-from distutils.dir_util import DistutilsFileError
import toml
# Import the JetpadClient from the parent directory
diff --git a/fragment/fragment-ktx/build.gradle b/fragment/fragment-ktx/build.gradle
index 97c3e80..4c8fc2f 100644
--- a/fragment/fragment-ktx/build.gradle
+++ b/fragment/fragment-ktx/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(project(":fragment:fragment"))
- api("androidx.activity:activity-ktx:1.5.0") {
+ api("androidx.activity:activity-ktx:1.5.1") {
because "Mirror fragment dependency graph for -ktx artifacts"
}
api("androidx.core:core-ktx:1.2.0") {
diff --git a/fragment/fragment/build.gradle b/fragment/fragment/build.gradle
index c9e0c84..02e8a8a 100644
--- a/fragment/fragment/build.gradle
+++ b/fragment/fragment/build.gradle
@@ -29,7 +29,7 @@
api("androidx.collection:collection:1.1.0")
api("androidx.viewpager:viewpager:1.0.0")
api("androidx.loader:loader:1.0.0")
- api("androidx.activity:activity:1.5.0")
+ api("androidx.activity:activity:1.5.1")
api("androidx.lifecycle:lifecycle-livedata-core:2.5.1")
api("androidx.lifecycle:lifecycle-viewmodel:2.5.1")
api("androidx.lifecycle:lifecycle-viewmodel-savedstate:2.5.1")
diff --git a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
index 1ec1fe6b..8bfb087 100644
--- a/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
+++ b/metrics/metrics-performance/src/main/java/androidx/metrics/performance/PerformanceMetricsState.kt
@@ -48,7 +48,7 @@
* Temporary per-frame to track UI and user state.
* Unlike the states tracked in `states`, any state in this structure is only valid until
* the next frame, at which point it is cleared. Any state data added here is automatically
- * removed; there is no matching "remove" method for [.addSingleFrameState]
+ * removed; there is no matching "remove" method for [.putSingleFrameState]
*
* @see putSingleFrameState
*/
@@ -164,7 +164,13 @@
* State information can be about UI elements that are currently active (such as the current
* [Activity] or layout) or a user interaction like flinging a list.
* If the PerformanceMetricsState object already contains an entry with the same key,
- * the old value is replaced by the new one.
+ * the old value is replaced by the new one. Note that this means apps with several
+ * instances of similar objects (such as multipe `RecyclerView`s) should
+ * therefore use unique keys for these instances to avoid clobbering state values
+ * for other instances and to provide enough information for later analysis which
+ * allows for disambiguation between these objects. For example, using "RVHeaders" and
+ * "RVContent" might be more helpful than just "RecyclerView" for a messaging app using
+ * `RecyclerView` objects for both a headers list and a list of message contents.
*
* Some state may be provided automatically by other AndroidX libraries.
* But applications are encouraged to add user state specific to those applications
diff --git a/navigation/navigation-compose/build.gradle b/navigation/navigation-compose/build.gradle
index 3a2b25f..3d7416c 100644
--- a/navigation/navigation-compose/build.gradle
+++ b/navigation/navigation-compose/build.gradle
@@ -28,7 +28,7 @@
implementation(libs.kotlinStdlib)
implementation("androidx.compose.foundation:foundation-layout:1.0.1")
- api("androidx.activity:activity-compose:1.5.0")
+ api("androidx.activity:activity-compose:1.5.1")
api("androidx.compose.animation:animation:1.0.1")
api("androidx.compose.runtime:runtime:1.0.1")
api("androidx.compose.runtime:runtime-saveable:1.0.1")
diff --git a/navigation/navigation-runtime/build.gradle b/navigation/navigation-runtime/build.gradle
index 70c2046..94c45c4 100644
--- a/navigation/navigation-runtime/build.gradle
+++ b/navigation/navigation-runtime/build.gradle
@@ -25,7 +25,7 @@
dependencies {
api(project(":navigation:navigation-common"))
- api("androidx.activity:activity-ktx:1.5.0")
+ api("androidx.activity:activity-ktx:1.5.1")
api("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
api("androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1")
api("androidx.annotation:annotation-experimental:1.1.0")
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
index e43619f..c500f7e 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/MethodProcessorDelegate.kt
@@ -36,8 +36,10 @@
import androidx.room.solver.query.result.QueryResultBinder
import androidx.room.solver.shortcut.binder.CallableDeleteOrUpdateMethodBinder.Companion.createDeleteOrUpdateBinder
import androidx.room.solver.shortcut.binder.CallableInsertMethodBinder.Companion.createInsertBinder
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.transaction.binder.CoroutineTransactionMethodBinder
import androidx.room.solver.transaction.binder.InstantTransactionMethodBinder
import androidx.room.solver.transaction.binder.TransactionMethodBinder
@@ -91,6 +93,11 @@
abstract fun findDeleteOrUpdateMethodBinder(returnType: XType): DeleteOrUpdateMethodBinder
+ abstract fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder
+
abstract fun findTransactionMethodBinder(
callType: TransactionMethod.CallType
): TransactionMethodBinder
@@ -176,6 +183,11 @@
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
context.typeAdapterStore.findDeleteOrUpdateMethodBinder(returnType)
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = context.typeAdapterStore.findUpsertMethodBinder(returnType, params)
+
override fun findTransactionMethodBinder(callType: TransactionMethod.CallType) =
InstantTransactionMethodBinder(
TransactionMethodAdapter(executableElement.jvmName, callType)
@@ -255,6 +267,23 @@
)
}
+ override fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = createUpsertBinder(
+ typeArg = returnType,
+ adapter = context.typeAdapterStore.findUpsertAdapter(returnType, params)
+ ) { callableImpl, dbField ->
+ addStatement(
+ "return $T.execute($N, $L, $L, $N)",
+ RoomCoroutinesTypeNames.COROUTINES_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl,
+ continuationParam.name
+ )
+ }
+
override fun findDeleteOrUpdateMethodBinder(returnType: XType) =
createDeleteOrUpdateBinder(
typeArg = returnType,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
index 2f1aff8..879a7b3 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/processor/ShortcutMethodProcessor.kt
@@ -208,6 +208,11 @@
params: List<ShortcutQueryParameter>
) = delegate.findInsertMethodBinder(returnType, params)
+ fun findUpsertMethodBinder(
+ returnType: XType,
+ params: List<ShortcutQueryParameter>
+ ) = delegate.findUpsertMethodBinder(returnType, params)
+
fun findDeleteOrUpdateMethodBinder(returnType: XType) =
delegate.findDeleteOrUpdateMethodBinder(returnType)
}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
index 1e1ac0d..cc06a34 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/TypeAdapterStore.kt
@@ -81,16 +81,22 @@
import androidx.room.solver.query.result.SingleNamedColumnRowAdapter
import androidx.room.solver.shortcut.binder.DeleteOrUpdateMethodBinder
import androidx.room.solver.shortcut.binder.InsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
import androidx.room.solver.shortcut.binderprovider.DeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.UpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.InstantInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.InstantUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.shortcut.result.DeleteOrUpdateMethodAdapter
import androidx.room.solver.shortcut.result.InsertMethodAdapter
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
import androidx.room.solver.types.BoxedBooleanToBoxedIntConverter
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.ByteArrayColumnTypeAdapter
@@ -233,6 +239,13 @@
add(InstantDeleteOrUpdateMethodBinderProvider(context))
}
+ val upsertBinderProviders: List<UpsertMethodBinderProvider> =
+ mutableListOf<UpsertMethodBinderProvider>().apply {
+ addAll(RxCallableUpsertMethodBinderProvider.getAll(context))
+ add(GuavaListenableFutureUpsertMethodBinderProvider(context))
+ add(InstantUpsertMethodBinderProvider(context))
+ }
+
/**
* Searches 1 way to bind a value into a statement.
*/
@@ -391,6 +404,15 @@
}.provide(typeMirror, params)
}
+ fun findUpsertMethodBinder(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return upsertBinderProviders.first {
+ it.matches(typeMirror)
+ }.provide(typeMirror, params)
+ }
+
fun findQueryResultBinder(
typeMirror: XType,
query: ParsedQuery,
@@ -432,6 +454,15 @@
return InsertMethodAdapter.create(typeMirror, params)
}
+ @Suppress("UNUSED_PARAMETER") // param will be used in a future change
+ fun findUpsertAdapter(
+ typeMirror: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodAdapter? {
+ // TODO: change for UpsertMethodAdapter when bind has been created
+ return null
+ }
+
fun findQueryResultAdapter(
typeMirror: XType,
query: ParsedQuery,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
new file mode 100644
index 0000000..37aa163
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/CallableUpsertMethodBinder.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.room.solver.shortcut.binder
+
+import androidx.room.ext.CallableTypeSpecBuilder
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import com.squareup.javapoet.CodeBlock
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder for deferred upsert methods.
+ *
+ * This binder will create a Callable implementation that delegates to the
+ * [UpsertMethodAdapter]. Usage of the Callable impl is then delegate to the [addStmntBlock]
+ * function.
+ */
+class CallableUpsertMethodBinder(
+ val typeArg: XType,
+ val addStmntBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit,
+ adapter: UpsertMethodAdapter?
+) : UpsertMethodBinder(adapter) {
+
+ companion object {
+ fun createUpsertBinder(
+ typeArg: XType,
+ adapter: UpsertMethodAdapter?,
+ addCodeBlock: CodeBlock.Builder.(callableImpl: TypeSpec, dbField: FieldSpec) -> Unit
+ ) = CallableUpsertMethodBinder(typeArg, addCodeBlock, adapter)
+ }
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ val adapterScope = scope.fork()
+ val callableImpl = CallableTypeSpecBuilder(typeArg.typeName) {
+ // TODO add the createMethodBody in UpsertMethodAdapter
+ addCode(adapterScope.generate())
+ }.build()
+
+ scope.builder().apply {
+ addStmntBlock(callableImpl, dbField)
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
new file mode 100644
index 0000000..06860f7
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/InstantUpsertMethodBinder.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2018 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.room.solver.shortcut.binder
+
+import androidx.room.ext.N
+import androidx.room.solver.CodeGenScope
+import androidx.room.solver.shortcut.result.UpsertMethodAdapter
+import androidx.room.vo.ShortcutQueryParameter
+import androidx.room.writer.DaoWriter
+import com.squareup.javapoet.FieldSpec
+import com.squareup.javapoet.TypeSpec
+
+/**
+ * Binder that knows how to write instant (blocking) upsert methods.
+ */
+class InstantUpsertMethodBinder(adapter: UpsertMethodAdapter?) : UpsertMethodBinder(adapter) {
+
+ override fun convertAndReturn(
+ parameters: List<ShortcutQueryParameter>,
+ upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
+ dbField: FieldSpec,
+ scope: CodeGenScope
+ ) {
+ scope.builder().apply {
+ addStatement("$N.assertNotSuspendingTransaction()", DaoWriter.dbField)
+ }
+ // TODO: createUpsertionMethodBody
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
index ff8c34f..9ec0168 100644
--- a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binder/UpsertMethodBinder.kt
@@ -21,8 +21,33 @@
import androidx.room.vo.ShortcutQueryParameter
import com.squareup.javapoet.FieldSpec
import com.squareup.javapoet.TypeSpec
-
+/**
+ * Connects the upsert method, the database and the [UpsertMethodAdapter].
+ */
abstract class UpsertMethodBinder(val adapter: UpsertMethodAdapter?) {
+
+ /**
+ * Received the upsert method parameters, the upsertion adapters and generations the code that
+ * runs the upsert and returns the result.
+ *
+ * For example, for the DAO method:
+ * ```
+ * @Upsert
+ * fun addPublishers(vararg publishers: Publisher): List<Long>
+ * ```
+ * The following code will be generated:
+ *
+ * ```
+ * __db.beginTransaction();
+ * try {
+ * List<Long> _result = __upsertionAdapterOfPublisher.upsertAndReturnIdsList(publishers);
+ * __db.setTransactionSuccessful();
+ * return _result;
+ * } finally {
+ * __db.endTransaction();
+ * }
+ * ```
+ */
abstract fun convertAndReturn(
parameters: List<ShortcutQueryParameter>,
upsertionAdapters: Map<String, Pair<FieldSpec, TypeSpec>>,
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..8f8cee0
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/GuavaListenableFutureUpsertMethodBinderProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.GuavaUtilConcurrentTypeNames
+import androidx.room.ext.L
+import androidx.room.ext.N
+import androidx.room.ext.RoomGuavaTypeNames
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.processor.ProcessorErrors
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder.Companion.createUpsertBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Guava ListenableFuture binders.
+ */
+class GuavaListenableFutureUpsertMethodBinderProvider(
+ private val context: Context
+) : UpsertMethodBinderProvider {
+
+ private val hasGuavaRoom by lazy {
+ context.processingEnv.findTypeElement(RoomGuavaTypeNames.GUAVA_ROOM) != null
+ }
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 &&
+ declared.rawType.typeName == GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ if (!hasGuavaRoom) {
+ context.logger.e(ProcessorErrors.MISSING_ROOM_GUAVA_ARTIFACT)
+ }
+
+ val typeArg = declared.typeArguments.first()
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return createUpsertBinder(typeArg, adapter) { callableImpl, dbField ->
+ addStatement(
+ "return $T.createListenableFuture($N, $L, $L)",
+ RoomGuavaTypeNames.GUAVA_ROOM,
+ dbField,
+ "true", // inTransaction
+ callableImpl
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..4a3ef91
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/InstantUpsertMethodBinderProvider.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2018 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.processor.Context
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.solver.shortcut.binder.InstantUpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for instant (blocking) upsert method binder.
+ */
+class InstantUpsertMethodBinderProvider(private val context: Context) : UpsertMethodBinderProvider {
+
+ override fun matches(declared: XType) = true
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ return InstantUpsertMethodBinder(
+ context.typeAdapterStore.findUpsertAdapter(declared, params)
+ )
+ }
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..df1911e
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/RxCallableUpsertMethodBinderProvider.kt
@@ -0,0 +1,97 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XRawType
+import androidx.room.compiler.processing.XType
+import androidx.room.ext.L
+import androidx.room.ext.T
+import androidx.room.processor.Context
+import androidx.room.solver.RxType
+import androidx.room.solver.shortcut.binder.CallableUpsertMethodBinder
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for Rx Callable binders.
+ */
+open class RxCallableUpsertMethodBinderProvider internal constructor(
+ val context: Context,
+ private val rxType: RxType
+) : UpsertMethodBinderProvider {
+
+ /**
+ * [Single] and [Maybe] are generics but [Completable] is not so each implementation of this
+ * class needs to define how to extract the type argument.
+ */
+ open fun extractTypeArg(declared: XType): XType = declared.typeArguments.first()
+
+ override fun matches(declared: XType): Boolean =
+ declared.typeArguments.size == 1 && matchesRxType(declared)
+
+ private fun matchesRxType(declared: XType): Boolean {
+ return declared.rawType.typeName == rxType.className
+ }
+
+ override fun provide(
+ declared: XType,
+ params: List<ShortcutQueryParameter>
+ ): UpsertMethodBinder {
+ val typeArg = extractTypeArg(declared)
+ val adapter = context.typeAdapterStore.findUpsertAdapter(typeArg, params)
+ return CallableUpsertMethodBinder.createUpsertBinder(typeArg, adapter) { callableImpl, _ ->
+ addStatement("return $T.fromCallable($L)", rxType.className, callableImpl)
+ }
+ }
+
+ companion object {
+ fun getAll(context: Context) = listOf(
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX2_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX2_COMPLETABLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_SINGLE),
+ RxCallableUpsertMethodBinderProvider(context, RxType.RX3_MAYBE),
+ RxCompletableUpsertMethodBinderProvider(context, RxType.RX3_COMPLETABLE)
+ )
+ }
+}
+
+private class RxCompletableUpsertMethodBinderProvider(
+ context: Context,
+ rxType: RxType
+) : RxCallableUpsertMethodBinderProvider(context, rxType) {
+
+ private val completableType: XRawType? by lazy {
+ context.processingEnv.findType(rxType.className)?.rawType
+ }
+
+ /**
+ * Since Completable is not a generic, the supported return type should be Void.
+ * Like this, the generated Callable.call method will return Void.
+ */
+ override fun extractTypeArg(declared: XType): XType =
+ context.COMMON_TYPES.VOID
+
+ override fun matches(declared: XType): Boolean = isCompletable(declared)
+
+ private fun isCompletable(declared: XType): Boolean {
+ if (completableType == null) {
+ return false
+ }
+ return declared.rawType.isAssignableFrom(completableType!!)
+ }
+}
diff --git a/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
new file mode 100644
index 0000000..0858f92
--- /dev/null
+++ b/room/room-compiler/src/main/kotlin/androidx/room/solver/shortcut/binderprovider/UpsertMethodBinderProvider.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.room.solver.shortcut.binderprovider
+
+import androidx.room.compiler.processing.XType
+import androidx.room.solver.shortcut.binder.UpsertMethodBinder
+import androidx.room.vo.ShortcutQueryParameter
+
+/**
+ * Provider for upsert method binders.
+ */
+interface UpsertMethodBinderProvider {
+
+ /**
+ * Check whether the [XType] can be handled by the [UpsertMethodBinder]
+ */
+ fun matches(declared: XType): Boolean
+
+ /**
+ * Provider of [UpsertMethodBinder], based on the [XType] and the list of parameters
+ */
+ fun provide(declared: XType, params: List<ShortcutQueryParameter>): UpsertMethodBinder
+}
\ No newline at end of file
diff --git a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
index 993cdd2..2fa5dbb 100644
--- a/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
+++ b/room/room-compiler/src/test/kotlin/androidx/room/solver/TypeAdapterStoreTest.kt
@@ -56,8 +56,10 @@
import androidx.room.solver.query.result.MultiTypedPagingSourceQueryResultBinder
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.GuavaListenableFutureUpsertMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableDeleteOrUpdateMethodBinderProvider
import androidx.room.solver.shortcut.binderprovider.RxCallableInsertMethodBinderProvider
+import androidx.room.solver.shortcut.binderprovider.RxCallableUpsertMethodBinderProvider
import androidx.room.solver.types.BoxedPrimitiveColumnTypeAdapter
import androidx.room.solver.types.CompositeAdapter
import androidx.room.solver.types.CustomTypeConverterWrapper
@@ -793,6 +795,69 @@
}
@Test
+ fun testFindUpsertSingle() {
+ listOf(
+ Triple(COMMON.RX2_SINGLE, COMMON.RX2_ROOM, RxJava2TypeNames.SINGLE),
+ Triple(COMMON.RX3_SINGLE, COMMON.RX3_ROOM, RxJava3TypeNames.SINGLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ @Suppress("CHANGING_ARGUMENTS_EXECUTION_ORDER_FOR_NAMED_VARARGS")
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val single = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(single).isNotNull()
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(single.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertMaybe() {
+ listOf(
+ Triple(COMMON.RX2_MAYBE, COMMON.RX2_ROOM, RxJava2TypeNames.MAYBE),
+ Triple(COMMON.RX3_MAYBE, COMMON.RX3_ROOM, RxJava3TypeNames.MAYBE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val maybe = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(maybe.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertCompletable() {
+ listOf(
+ Triple(COMMON.RX2_COMPLETABLE, COMMON.RX2_ROOM, RxJava2TypeNames.COMPLETABLE),
+ Triple(COMMON.RX3_COMPLETABLE, COMMON.RX3_ROOM, RxJava3TypeNames.COMPLETABLE)
+ ).forEach { (rxTypeSrc, _, rxTypeClassName) ->
+ runProcessorTest(sources = listOf(rxTypeSrc)) { invocation ->
+ val completable = invocation.processingEnv.requireTypeElement(rxTypeClassName)
+ assertThat(
+ RxCallableUpsertMethodBinderProvider.getAll(invocation.context).any {
+ it.matches(completable.type)
+ }).isTrue()
+ }
+ }
+ }
+
+ @Test
+ fun testFindUpsertListenableFuture() {
+ runProcessorTest(sources = listOf(COMMON.LISTENABLE_FUTURE)) {
+ invocation ->
+ val future = invocation.processingEnv
+ .requireTypeElement(GuavaUtilConcurrentTypeNames.LISTENABLE_FUTURE)
+ assertThat(
+ GuavaListenableFutureUpsertMethodBinderProvider(invocation.context).matches(
+ future.type
+ )).isTrue()
+ }
+ }
+
+ @Test
fun testFindLiveData() {
runProcessorTest(
sources = listOf(COMMON.COMPUTABLE_LIVE_DATA, COMMON.LIVE_DATA)
diff --git a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
index 720e795..d9fc71c 100644
--- a/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
+++ b/tracing/tracing-perfetto-common/src/main/java/androidx/tracing/perfetto/PerfettoHandshake.kt
@@ -260,7 +260,15 @@
public class EnableTracingResponse @RestrictTo(LIBRARY_GROUP) constructor(
@EnableTracingResultCode public val exitCode: Int,
+
+ /**
+ * This can be `null` iff we cannot communicate with the broadcast receiver of the target
+ * process (e.g. app does not offer Perfetto tracing) or if we cannot parse the response
+ * from the receiver. In either case, tracing is unlikely to work under these circumstances,
+ * and more context on how to proceed can be found in [exitCode] or [message] properties.
+ */
public val requiredVersion: String?,
+
public val message: String?
)
}
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
index 4d95812..a940dba 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatDarkThemeTest.java
@@ -28,6 +28,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.filters.SdkSuppress;
+import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -61,6 +62,7 @@
* Test the algorithmic darkening on web content that doesn't support dark style.
*/
@Test
+ @Ignore("b/235864049") // Find a way to run with targetSdk T
public void testSimplifiedDarkMode_rendersDark() throws Throwable {
WebkitUtils.checkFeature(WebViewFeature.ALGORITHMIC_DARKENING);
WebkitUtils.checkFeature(WebViewFeature.OFF_SCREEN_PRERASTER);
diff --git a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
index 6808524..d67a87b 100644
--- a/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
+++ b/webkit/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatLightThemeTest.java
@@ -40,7 +40,7 @@
public WebSettingsCompatLightThemeTest() {
// targetSdkVersion to T, it is min version the algorithmic darkening works.
// TODO(http://b/214741472): Use VERSION_CODES.TIRAMISU once available.
- super(WebViewLightThemeTestActivity.class, VERSION_CODES.CUR_DEVELOPMENT);
+ super(WebViewLightThemeTestActivity.class, 33);
}
/**
diff --git a/window/extensions/extensions/api/current.txt b/window/extensions/extensions/api/current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/current.txt
+++ b/window/extensions/extensions/api/current.txt
@@ -2,6 +2,7 @@
package androidx.window.extensions {
public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -12,6 +13,90 @@
}
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ }
+
+ public class ActivityStack {
+ ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public class SplitInfo {
+ ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public int getLayoutDirection();
+ method public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+}
+
package androidx.window.extensions.layout {
public interface DisplayFeature {
diff --git a/window/extensions/extensions/api/public_plus_experimental_current.txt b/window/extensions/extensions/api/public_plus_experimental_current.txt
index 6a28e00..c067593 100644
--- a/window/extensions/extensions/api/public_plus_experimental_current.txt
+++ b/window/extensions/extensions/api/public_plus_experimental_current.txt
@@ -1,11 +1,8 @@
// Signature format: 4.0
package androidx.window.extensions {
- @RequiresOptIn @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.CLASS) public @interface ExperimentalWindowExtensionsApi {
- }
-
public interface WindowExtensions {
- method @androidx.window.extensions.ExperimentalWindowExtensionsApi public androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -18,13 +15,13 @@
package androidx.window.extensions.embedding {
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public interface ActivityEmbeddingComponent {
+ public interface ActivityEmbeddingComponent {
method public boolean isActivityEmbedded(android.app.Activity);
method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
method public boolean shouldAlwaysExpand();
@@ -36,23 +33,23 @@
method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class ActivityStack {
+ public class ActivityStack {
ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
method public java.util.List<android.app.Activity!> getActivities();
method public boolean isEmpty();
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class EmbeddingRule {
+ public abstract class EmbeddingRule {
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitInfo {
+ public class SplitInfo {
ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
method public float getSplitRatio();
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
method public int getFinishPrimaryWithSecondary();
method public int getFinishSecondaryWithPrimary();
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
@@ -72,7 +69,7 @@
method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
method public int getFinishPrimaryWithSecondary();
method public android.content.Intent getPlaceholderIntent();
method public boolean isSticky();
@@ -89,7 +86,7 @@
method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
}
- @androidx.window.extensions.ExperimentalWindowExtensionsApi public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
method public int getLayoutDirection();
method public float getSplitRatio();
diff --git a/window/extensions/extensions/api/restricted_current.txt b/window/extensions/extensions/api/restricted_current.txt
index 1534daf..c067593 100644
--- a/window/extensions/extensions/api/restricted_current.txt
+++ b/window/extensions/extensions/api/restricted_current.txt
@@ -2,6 +2,7 @@
package androidx.window.extensions {
public interface WindowExtensions {
+ method public default androidx.window.extensions.embedding.ActivityEmbeddingComponent? getActivityEmbeddingComponent();
method public default int getVendorApiLevel();
method public androidx.window.extensions.layout.WindowLayoutComponent? getWindowLayoutComponent();
}
@@ -12,6 +13,90 @@
}
+package androidx.window.extensions.embedding {
+
+ public interface ActivityEmbeddingComponent {
+ method public boolean isActivityEmbedded(android.app.Activity);
+ method public void setEmbeddingRules(java.util.Set<androidx.window.extensions.embedding.EmbeddingRule!>);
+ method public void setSplitInfoCallback(java.util.function.Consumer<java.util.List<androidx.window.extensions.embedding.SplitInfo!>!>);
+ }
+
+ public class ActivityRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ method public boolean shouldAlwaysExpand();
+ }
+
+ public static final class ActivityRule.Builder {
+ ctor public ActivityRule.Builder(java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>);
+ method public androidx.window.extensions.embedding.ActivityRule build();
+ method public androidx.window.extensions.embedding.ActivityRule.Builder setShouldAlwaysExpand(boolean);
+ }
+
+ public class ActivityStack {
+ ctor public ActivityStack(java.util.List<android.app.Activity!>, boolean);
+ method public java.util.List<android.app.Activity!> getActivities();
+ method public boolean isEmpty();
+ }
+
+ public abstract class EmbeddingRule {
+ }
+
+ public class SplitInfo {
+ ctor public SplitInfo(androidx.window.extensions.embedding.ActivityStack, androidx.window.extensions.embedding.ActivityStack, float);
+ method public androidx.window.extensions.embedding.ActivityStack getPrimaryActivityStack();
+ method public androidx.window.extensions.embedding.ActivityStack getSecondaryActivityStack();
+ method public float getSplitRatio();
+ }
+
+ public class SplitPairRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public int getFinishSecondaryWithPrimary();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityIntentPair(android.app.Activity, android.content.Intent);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivityPair(android.app.Activity, android.app.Activity);
+ method public boolean shouldClearTop();
+ }
+
+ public static final class SplitPairRule.Builder {
+ ctor public SplitPairRule.Builder(java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.app.Activity!>!>, java.util.function.Predicate<android.util.Pair<android.app.Activity!,android.content.Intent!>!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPairRule build();
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setFinishSecondaryWithPrimary(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldClearTop(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishPrimaryWithSecondary(boolean);
+ method @Deprecated public androidx.window.extensions.embedding.SplitPairRule.Builder setShouldFinishSecondaryWithPrimary(boolean);
+ method public androidx.window.extensions.embedding.SplitPairRule.Builder setSplitRatio(float);
+ }
+
+ public class SplitPlaceholderRule extends androidx.window.extensions.embedding.SplitRule {
+ method public int getFinishPrimaryWithSecondary();
+ method public android.content.Intent getPlaceholderIntent();
+ method public boolean isSticky();
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesActivity(android.app.Activity);
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean matchesIntent(android.content.Intent);
+ }
+
+ public static final class SplitPlaceholderRule.Builder {
+ ctor public SplitPlaceholderRule.Builder(android.content.Intent, java.util.function.Predicate<android.app.Activity!>, java.util.function.Predicate<android.content.Intent!>, java.util.function.Predicate<android.view.WindowMetrics!>);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule build();
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setFinishPrimaryWithSecondary(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setLayoutDirection(int);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSplitRatio(float);
+ method public androidx.window.extensions.embedding.SplitPlaceholderRule.Builder setSticky(boolean);
+ }
+
+ public abstract class SplitRule extends androidx.window.extensions.embedding.EmbeddingRule {
+ method @RequiresApi(api=android.os.Build.VERSION_CODES.N) public boolean checkParentMetrics(android.view.WindowMetrics);
+ method public int getLayoutDirection();
+ method public float getSplitRatio();
+ field public static final int FINISH_ADJACENT = 2; // 0x2
+ field public static final int FINISH_ALWAYS = 1; // 0x1
+ field public static final int FINISH_NEVER = 0; // 0x0
+ }
+
+}
+
package androidx.window.extensions.layout {
public interface DisplayFeature {
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
deleted file mode 100644
index 0da12db..0000000
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/ExperimentalWindowExtensionsApi.java
+++ /dev/null
@@ -1,26 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.window.extensions;
-
-import androidx.annotation.RequiresOptIn;
-
-/**
- * Denotes that the API uses experimental WindowManager extension APIs.
- */
-@RequiresOptIn
-public @interface ExperimentalWindowExtensionsApi {
-}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
index 6ac25bd..c1a9bd8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/WindowExtensions.java
@@ -58,6 +58,7 @@
* @return the OEM implementation of {@link ActivityEmbeddingComponent}
*/
@Nullable
- @ExperimentalWindowExtensionsApi
- ActivityEmbeddingComponent getActivityEmbeddingComponent();
+ default ActivityEmbeddingComponent getActivityEmbeddingComponent() {
+ return null;
+ }
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
index f4dd8a4..e87736c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityEmbeddingComponent.java
@@ -19,7 +19,6 @@
import android.app.Activity;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.List;
import java.util.Set;
@@ -32,7 +31,6 @@
* <p>This interface should be implemented by OEM and deployed to the target devices.
* @see androidx.window.extensions.WindowExtensions
*/
-@ExperimentalWindowExtensionsApi
public interface ActivityEmbeddingComponent {
/**
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
index dbf8233..2d1fc33 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityRule.java
@@ -23,14 +23,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
/**
* Split configuration rule for individual activities.
*/
-@ExperimentalWindowExtensionsApi
public class ActivityRule extends EmbeddingRule {
@NonNull
private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
index 928a7b9..db275bb 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/ActivityStack.java
@@ -19,7 +19,6 @@
import android.app.Activity;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.ArrayList;
import java.util.List;
@@ -28,7 +27,6 @@
* Description of a group of activities stacked on top of each other and shown as a single
* container, all within the same task.
*/
-@ExperimentalWindowExtensionsApi
public class ActivityStack {
@NonNull
private final List<Activity> mActivities;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
index beac689..571eda0 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/EmbeddingRule.java
@@ -16,13 +16,10 @@
package androidx.window.extensions.embedding;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
-
/**
* Base interface for activity embedding rules. Used to group different types of rules together when
* updating from the core library.
*/
-@ExperimentalWindowExtensionsApi
public abstract class EmbeddingRule {
EmbeddingRule() {}
}
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
index 351fd1b..cfd8b1a 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitInfo.java
@@ -17,10 +17,8 @@
package androidx.window.extensions.embedding;
import androidx.annotation.NonNull;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
/** Describes a split of two containers with activities. */
-@ExperimentalWindowExtensionsApi
public class SplitInfo {
@NonNull
private final ActivityStack mPrimaryActivityStack;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
index 2cbb747..db9f65c 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPairRule.java
@@ -25,14 +25,12 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
/**
* Split configuration rules for activity pairs.
*/
-@ExperimentalWindowExtensionsApi
public class SplitPairRule extends SplitRule {
@NonNull
private final Predicate<Pair<Activity, Activity>> mActivityPairPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
index 83bfc32..1f8a8fe 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitPlaceholderRule.java
@@ -24,7 +24,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.util.function.Predicate;
@@ -32,7 +31,6 @@
* Split configuration rules for split placeholders - activities used to occupy additional
* available space on the side before the user selects content to show.
*/
-@ExperimentalWindowExtensionsApi
public class SplitPlaceholderRule extends SplitRule {
@NonNull
private final Predicate<Activity> mActivityPredicate;
diff --git a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
index 4b1d4e5..39b7df8 100644
--- a/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
+++ b/window/extensions/extensions/src/main/java/androidx/window/extensions/embedding/SplitRule.java
@@ -24,7 +24,6 @@
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
-import androidx.window.extensions.ExperimentalWindowExtensionsApi;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -37,7 +36,6 @@
* new activities started from the same process automatically by the embedding implementation on
* the device.
*/
-@ExperimentalWindowExtensionsApi
public abstract class SplitRule extends EmbeddingRule {
@NonNull
private final Predicate<WindowMetrics> mParentWindowMetricsPredicate;
diff --git a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
index 1b5b5ac..a917e8e 100644
--- a/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
+++ b/work/work-runtime/src/androidTest/java/androidx/work/impl/utils/ForceStopRunnableTest.java
@@ -25,6 +25,7 @@
import static org.hamcrest.Matchers.greaterThan;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -37,6 +38,7 @@
import android.content.Context;
import android.content.Intent;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
+import android.database.sqlite.SQLiteException;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -199,6 +201,30 @@
}
@Test
+ public void test_InitializationExceptionHandler_migrationFailures() {
+ mContext = mock(Context.class);
+ when(mContext.getApplicationContext()).thenReturn(mContext);
+ mWorkDatabase = WorkDatabase.create(mContext, mConfiguration.getTaskExecutor(), true);
+ when(mWorkManager.getWorkDatabase()).thenReturn(mWorkDatabase);
+ mRunnable = new ForceStopRunnable(mContext, mWorkManager);
+
+ InitializationExceptionHandler handler = mock(InitializationExceptionHandler.class);
+ Configuration configuration = new Configuration.Builder(mConfiguration)
+ .setInitializationExceptionHandler(handler)
+ .build();
+
+ when(mWorkManager.getConfiguration()).thenReturn(configuration);
+ // This is what WorkDatabasePathHelper uses under the hood to migrate the database.
+ when(mContext.getDatabasePath(anyString())).thenThrow(
+ new SQLiteException("Unable to migrate database"));
+
+ ForceStopRunnable runnable = spy(mRunnable);
+ doNothing().when(runnable).sleep(anyLong());
+ runnable.run();
+ verify(handler, times(1)).handleException(any(Throwable.class));
+ }
+
+ @Test
public void test_completeOnMultiProcessChecks() {
ForceStopRunnable runnable = spy(mRunnable);
doReturn(false).when(runnable).multiProcessChecks();
diff --git a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
index 805b8ee..cf9679b 100644
--- a/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
+++ b/work/work-runtime/src/main/java/androidx/work/impl/utils/ForceStopRunnable.java
@@ -38,6 +38,7 @@
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabaseCorruptException;
import android.database.sqlite.SQLiteDatabaseLockedException;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteTableLockedException;
import android.os.Build;
import android.text.TextUtils;
@@ -102,8 +103,29 @@
return;
}
while (true) {
- // Migrate the database to the no-backup directory if necessary.
- WorkDatabasePathHelper.migrateDatabase(mContext);
+
+ try {
+ // Migrate the database to the no-backup directory if necessary.
+ // Migrations are not retry-able. So if something unexpected were to happen
+ // here, the best we can do is to hand things off to the
+ // InitializationExceptionHandler.
+ WorkDatabasePathHelper.migrateDatabase(mContext);
+ } catch (SQLiteException sqLiteException) {
+ // This should typically never happen.
+ String message = "Unexpected SQLite exception during migrations";
+ Logger.get().error(TAG, message);
+ IllegalStateException exception =
+ new IllegalStateException(message, sqLiteException);
+ InitializationExceptionHandler exceptionHandler =
+ mWorkManager.getConfiguration().getInitializationExceptionHandler();
+ if (exceptionHandler != null) {
+ exceptionHandler.handleException(exception);
+ break;
+ } else {
+ throw exception;
+ }
+ }
+
// Clean invalid jobs attributed to WorkManager, and Workers that might have been
// interrupted because the application crashed (RUNNING state).
Logger.get().debug(TAG, "Performing cleanup operations.");
@@ -111,11 +133,11 @@
forceStopRunnable();
break;
} catch (SQLiteCantOpenDatabaseException
- | SQLiteDatabaseCorruptException
- | SQLiteDatabaseLockedException
- | SQLiteTableLockedException
- | SQLiteConstraintException
- | SQLiteAccessPermException exception) {
+ | SQLiteDatabaseCorruptException
+ | SQLiteDatabaseLockedException
+ | SQLiteTableLockedException
+ | SQLiteConstraintException
+ | SQLiteAccessPermException exception) {
mRetryCount++;
if (mRetryCount >= MAX_ATTEMPTS) {
// ForceStopRunnable is usually the first thing that accesses a database